Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions build-logic/src/main/kotlin/AndroidFeatureConventionPlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ internal class AndroidFeatureConventionPlugin : Plugin<Project> {
implementation(project(path = ":core:model"))
implementation(project(path = ":core:ui"))

implementation(libs.compose.effects)

implementation(libs.bundles.circuit)

api(libs.circuit.codegen.annotation)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<UserProfileModel>
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -15,4 +17,8 @@ internal abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindAuthRepository(defaultAuthRepository: DefaultAuthRepository): AuthRepository

@Binds
@Singleton
abstract fun bindUserRepository(defaultUserRepository: DefaultUserRepository): UserRepository
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
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(
accessToken = accessToken,
refreshToken = refreshToken,
)
}

internal fun UserProfileResponse.toModel(): UserProfileModel {
return UserProfileModel(
id = id,
email = email,
nickname = nickname,
provider = provider,
)
}
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -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,
)
Original file line number Diff line number Diff line change
@@ -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,
)
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -21,14 +23,51 @@ 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<LibraryScreen.State> {

@Composable
override fun present(): LibraryScreen.State {
val scope = rememberCoroutineScope()
var isLoading by rememberRetained { mutableStateOf(false) }
var sideEffect by rememberRetained { mutableStateOf<LibraryScreen.SideEffect?>(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) {
Expand All @@ -40,9 +79,9 @@ class LibraryPresenter @AssistedInject constructor(
scope.launch {
try {
isLoading = true
repository.logout()
authRepository.logout()
.onSuccess {
repository.clearTokens()
authRepository.clearTokens()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

로그인/로그아웃 관련 비즈니스 로직을 DefaultAuthRepository 내부에서 처리하는건 어떻게 생각해? 개인적으로 서버통신&로컬토큰 처리까지 캡슐화하고 Presenter에서는 logout()/login()만 호출하는게 나을 것 같아

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

좋은 방향 같구만

Copy link
Copy Markdown
Contributor Author

@easyhooon easyhooon Jul 7, 2025

Choose a reason for hiding this comment

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

@seoyoon513 근데 관련해서 고민을 해봐야할게
repository 에서 토큰 제거 처리를 한다는 것은 현재 구현 로직상 api 성공/실패 여부와 상관없이 로그아웃을 진행한다는 의미라, 로그아웃 기능 정책이 달라지는 거거든

  1. 로그아웃 API 호출 -> 성공(서버에서 해당 토큰들을 블랙 리스트 처리) -> 클라이언트 디비내 토큰을 제거하고, 로그인 화면으로 이동 (현재의 구현 방식)
  2. 로그아웃 API 호출 -> 성공 실패 여부와 상관 없이, 클라이언트 디비내 토큰을 제거하고, 로그인 화면으로 이동(누나가 제안한 방법)

사실 로그아웃를 눌렀을때 API 호출이 실패(accessToken, refreshToken이 다 만료)하여 로그인 화면으로 이동하든, API 호출이 성공하여 로그인 화면으로 이동하든 결과만 놓고 보면 같은 것이라 기획적인 문제라고도 볼 수 있을 것 같은데(서버에서 토큰을 블랙리스트에 추가하는 여부만 다름), 이건 서버랑도 같이 얘기 나눠보면 좋을 것 같은 주제인것 같네

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

오키 이건 이번주 팀세션 때 회원탈퇴랑 같이 얘기해보자

navigator.resetRoot(LoginScreen)
}
.onFailure { exception ->
Expand Down Expand Up @@ -70,6 +109,8 @@ class LibraryPresenter @AssistedInject constructor(

return LibraryScreen.State(
isLoading = isLoading,
nickname = nickname,
email = email,
sideEffect = sideEffect,
eventSink = ::handleEvent,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -118,6 +128,8 @@ private fun LibraryPreview() {
BooketTheme {
Library(
state = LibraryScreen.State(
nickname = "홍길동",
email = "test@test.com",
eventSink = {},
),
)
Expand Down
3 changes: 2 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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" }
Expand Down
Loading