diff --git a/build-logic/src/main/kotlin/AndroidFeatureConventionPlugin.kt b/build-logic/src/main/kotlin/AndroidFeatureConventionPlugin.kt index 7da7499d..c14f559b 100644 --- a/build-logic/src/main/kotlin/AndroidFeatureConventionPlugin.kt +++ b/build-logic/src/main/kotlin/AndroidFeatureConventionPlugin.kt @@ -1,9 +1,10 @@ + +import com.ninecraft.booket.convention.api import com.ninecraft.booket.convention.applyPlugins import com.ninecraft.booket.convention.implementation -import com.ninecraft.booket.convention.api import com.ninecraft.booket.convention.ksp -import com.ninecraft.booket.convention.project import com.ninecraft.booket.convention.libs +import com.ninecraft.booket.convention.project import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.dependencies @@ -23,6 +24,7 @@ internal class AndroidFeatureConventionPlugin : Plugin { implementation(project(path = ":core:designsystem")) implementation(project(path = ":core:model")) implementation(project(path = ":core:ui")) + implementation(project(path = ":screens")) implementation(libs.compose.effects) diff --git a/build.gradle.kts b/build.gradle.kts index d88a61c2..c9b7344c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -41,3 +41,7 @@ allprojects { } } } + +apply { + from("gradle/projectDependencyGraph.gradle") +} diff --git a/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomePresenter.kt b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomePresenter.kt index f36fd434..3a5abef5 100644 --- a/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomePresenter.kt +++ b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomePresenter.kt @@ -2,6 +2,7 @@ package com.ninecraft.booket.feature.home import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope +import com.ninecraft.booket.screens.HomeScreen import com.slack.circuit.codegen.annotations.CircuitInject import com.slack.circuit.runtime.Navigator import com.slack.circuit.runtime.presenter.Presenter @@ -13,13 +14,13 @@ import dagger.hilt.android.components.ActivityRetainedComponent @Suppress("unused") class HomePresenter @AssistedInject constructor( @Assisted private val navigator: Navigator, -) : Presenter { +) : Presenter { @Composable - override fun present(): HomeScreen.State { + override fun present(): HomeUiState { val scope = rememberCoroutineScope() - return HomeScreen.State {} + return HomeUiState {} } @CircuitInject(HomeScreen::class, ActivityRetainedComponent::class) diff --git a/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeScreen.kt b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeScreen.kt index 536e1720..67043a79 100644 --- a/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeScreen.kt +++ b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeScreen.kt @@ -9,26 +9,14 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.ninecraft.booket.core.designsystem.DevicePreview import com.ninecraft.booket.core.designsystem.theme.ReedTheme +import com.ninecraft.booket.screens.HomeScreen import com.slack.circuit.codegen.annotations.CircuitInject -import com.slack.circuit.runtime.CircuitUiEvent -import com.slack.circuit.runtime.CircuitUiState -import com.slack.circuit.runtime.screen.Screen import dagger.hilt.android.components.ActivityRetainedComponent -import kotlinx.parcelize.Parcelize - -@Parcelize -data object HomeScreen : Screen { - data class State( - val eventSink: (Event) -> Unit, - ) : CircuitUiState - - sealed interface Event : CircuitUiEvent -} @CircuitInject(HomeScreen::class, ActivityRetainedComponent::class) @Composable internal fun Home( - state: HomeScreen.State, + state: HomeUiState, modifier: Modifier = Modifier, ) { Column( @@ -46,7 +34,7 @@ internal fun Home( @Suppress("unused") @Composable internal fun HomeContent( - state: HomeScreen.State, + state: HomeUiState, modifier: Modifier = Modifier, ) { Text(text = "홈") @@ -57,7 +45,7 @@ internal fun HomeContent( private fun HomePreview() { ReedTheme { Home( - state = HomeScreen.State( + state = HomeUiState( eventSink = {}, ), ) diff --git a/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUiState.kt b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUiState.kt new file mode 100644 index 00000000..67db3e16 --- /dev/null +++ b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUiState.kt @@ -0,0 +1,10 @@ +package com.ninecraft.booket.feature.home + +import com.slack.circuit.runtime.CircuitUiEvent +import com.slack.circuit.runtime.CircuitUiState + +data class HomeUiState( + val eventSink: (HomeUiEvent) -> Unit, +) : CircuitUiState + +sealed interface HomeUiEvent : CircuitUiEvent diff --git a/feature/library/build.gradle.kts b/feature/library/build.gradle.kts index 90c51bf5..5f01e5ca 100644 --- a/feature/library/build.gradle.kts +++ b/feature/library/build.gradle.kts @@ -16,8 +16,6 @@ ksp { dependencies { implementations( - projects.feature.login, - libs.logger, ) } diff --git a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/HandleLibrarySideEffects.kt b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/HandleLibrarySideEffects.kt index d264519d..b952177f 100644 --- a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/HandleLibrarySideEffects.kt +++ b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/HandleLibrarySideEffects.kt @@ -7,14 +7,14 @@ import androidx.compose.ui.platform.LocalContext @Composable internal fun HandleLibrarySideEffects( - state: LibraryScreen.State, - eventSink: (LibraryScreen.Event) -> Unit, + state: LibraryUiState, + eventSink: (LibraryUiEvent) -> Unit, ) { val context = LocalContext.current LaunchedEffect(state.sideEffect) { when (state.sideEffect) { - is LibraryScreen.SideEffect.ShowToast -> { + is LibrarySideEffect.ShowToast -> { Toast.makeText(context, state.sideEffect.message, Toast.LENGTH_SHORT).show() } @@ -22,7 +22,7 @@ internal fun HandleLibrarySideEffects( } if (state.sideEffect != null) { - eventSink(LibraryScreen.Event.InitSideEffect) + eventSink(LibraryUiEvent.InitSideEffect) } } } 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 5763714d..1d13e121 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 @@ -8,7 +8,8 @@ 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.ninecraft.booket.screens.LibraryScreen +import com.ninecraft.booket.screens.LoginScreen import com.orhanobut.logger.Logger import com.skydoves.compose.effects.RememberedEffect import com.slack.circuit.codegen.annotations.CircuitInject @@ -25,13 +26,13 @@ class LibraryPresenter @AssistedInject constructor( @Assisted private val navigator: Navigator, private val authRepository: AuthRepository, private val userRepository: UserRepository, -) : Presenter { +) : Presenter { @Composable - override fun present(): LibraryScreen.State { + override fun present(): LibraryUiState { val scope = rememberCoroutineScope() var isLoading by rememberRetained { mutableStateOf(false) } - var sideEffect by rememberRetained { mutableStateOf(null) } + var sideEffect by rememberRetained { mutableStateOf(null) } var nickname by rememberRetained { mutableStateOf("") } var email by rememberRetained { mutableStateOf("") } @@ -47,7 +48,7 @@ class LibraryPresenter @AssistedInject constructor( .onFailure { exception -> val handleErrorMessage = { message: String -> Logger.e(message) - sideEffect = LibraryScreen.SideEffect.ShowToast(message) + sideEffect = LibrarySideEffect.ShowToast(message) } handleException( @@ -69,13 +70,13 @@ class LibraryPresenter @AssistedInject constructor( getUserProfile() } - fun handleEvent(event: LibraryScreen.Event) { + fun handleEvent(event: LibraryUiEvent) { when (event) { - is LibraryScreen.Event.InitSideEffect -> { + is LibraryUiEvent.InitSideEffect -> { sideEffect = null } - is LibraryScreen.Event.OnLogoutButtonClick -> { + is LibraryUiEvent.OnLogoutButtonClick -> { scope.launch { try { isLoading = true @@ -87,7 +88,7 @@ class LibraryPresenter @AssistedInject constructor( .onFailure { exception -> val handleErrorMessage = { message: String -> Logger.e(message) - sideEffect = LibraryScreen.SideEffect.ShowToast(message) + sideEffect = LibrarySideEffect.ShowToast(message) } handleException( @@ -107,7 +108,7 @@ class LibraryPresenter @AssistedInject constructor( } } - return LibraryScreen.State( + return LibraryUiState( isLoading = isLoading, nickname = nickname, email = email, 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 51b68634..6b2c8bb0 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 @@ -20,37 +20,14 @@ import com.ninecraft.booket.core.designsystem.component.button.ReedButton import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle import com.ninecraft.booket.core.designsystem.component.button.largeButtonStyle import com.ninecraft.booket.core.designsystem.theme.ReedTheme +import com.ninecraft.booket.screens.LibraryScreen import com.slack.circuit.codegen.annotations.CircuitInject -import com.slack.circuit.runtime.CircuitUiEvent -import com.slack.circuit.runtime.CircuitUiState -import com.slack.circuit.runtime.screen.Screen import dagger.hilt.android.components.ActivityRetainedComponent -import kotlinx.parcelize.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 - - sealed interface SideEffect { - data class ShowToast(val message: String) : SideEffect - } - - sealed interface Event : CircuitUiEvent { - data object InitSideEffect : Event - data object OnLogoutButtonClick : Event - } -} @CircuitInject(LibraryScreen::class, ActivityRetainedComponent::class) @Composable internal fun Library( - state: LibraryScreen.State, + state: LibraryUiState, modifier: Modifier = Modifier, ) { HandleLibrarySideEffects( @@ -72,7 +49,7 @@ internal fun Library( @Composable internal fun LibraryContent( - state: LibraryScreen.State, + state: LibraryUiState, modifier: Modifier = Modifier, ) { Column( @@ -94,7 +71,7 @@ internal fun LibraryContent( } ReedButton( onClick = { - state.eventSink(LibraryScreen.Event.OnLogoutButtonClick) + state.eventSink(LibraryUiEvent.OnLogoutButtonClick) }, modifier = Modifier .fillMaxWidth() @@ -118,7 +95,7 @@ internal fun LibraryContent( private fun LibraryPreview() { ReedTheme { Library( - state = LibraryScreen.State( + state = LibraryUiState( nickname = "홍길동", email = "test@test.com", eventSink = {}, diff --git a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUiState.kt b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUiState.kt new file mode 100644 index 00000000..dd710b04 --- /dev/null +++ b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUiState.kt @@ -0,0 +1,21 @@ +package com.ninecraft.booket.feature.library + +import com.slack.circuit.runtime.CircuitUiEvent +import com.slack.circuit.runtime.CircuitUiState + +data class LibraryUiState( + val isLoading: Boolean = false, + val nickname: String = "", + val email: String = "", + val sideEffect: LibrarySideEffect? = null, + val eventSink: (LibraryUiEvent) -> Unit, +) : CircuitUiState + +sealed interface LibrarySideEffect { + data class ShowToast(val message: String) : LibrarySideEffect +} + +sealed interface LibraryUiEvent : CircuitUiEvent { + data object InitSideEffect : LibraryUiEvent + data object OnLogoutButtonClick : LibraryUiEvent +} diff --git a/feature/login/build.gradle.kts b/feature/login/build.gradle.kts index fb3efc06..cd173eeb 100644 --- a/feature/login/build.gradle.kts +++ b/feature/login/build.gradle.kts @@ -16,8 +16,6 @@ ksp { dependencies { implementations( - projects.feature.home, - libs.logger, libs.kakao.auth, ) diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/HandleLoginSideEffects.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/HandleLoginSideEffects.kt index e39d0923..da6db198 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/HandleLoginSideEffects.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/HandleLoginSideEffects.kt @@ -8,27 +8,27 @@ import androidx.compose.ui.platform.LocalContext @Composable internal fun HandleLoginSideEffects( - state: LoginScreen.State, - eventSink: (LoginScreen.Event) -> Unit, + state: LoginUiState, + eventSink: (LoginUiEvent) -> Unit, ) { val context = LocalContext.current val kakaoLoginClient = remember { KakaoLoginClient() } LaunchedEffect(state.sideEffect) { when (state.sideEffect) { - is LoginScreen.SideEffect.KakaoLogin -> { + is LoginSideEffect.KakaoLogin -> { kakaoLoginClient.loginWithKakao( context = context, onSuccess = { token -> - eventSink(LoginScreen.Event.Login(token)) + eventSink(LoginUiEvent.Login(token)) }, onFailure = { errorMessage -> - eventSink(LoginScreen.Event.LoginFailure(errorMessage)) + eventSink(LoginUiEvent.LoginFailure(errorMessage)) }, ) } - is LoginScreen.SideEffect.ShowToast -> { + is LoginSideEffect.ShowToast -> { Toast.makeText(context, state.sideEffect.message, Toast.LENGTH_SHORT).show() } @@ -36,7 +36,7 @@ internal fun HandleLoginSideEffects( } if (state.sideEffect != null) { - eventSink(LoginScreen.Event.InitSideEffect) + eventSink(LoginUiEvent.InitSideEffect) } } } diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt index fac3af16..a3beb742 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt @@ -6,6 +6,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import com.ninecraft.booket.core.data.api.repository.AuthRepository +import com.ninecraft.booket.screens.LoginScreen +import com.ninecraft.booket.screens.TermsAgreementScreen import com.orhanobut.logger.Logger import com.slack.circuit.codegen.annotations.CircuitInject import com.slack.circuit.retained.rememberRetained @@ -20,31 +22,31 @@ import kotlinx.coroutines.launch class LoginPresenter @AssistedInject constructor( @Assisted private val navigator: Navigator, private val repository: AuthRepository, -) : Presenter { +) : Presenter { @Composable - override fun present(): LoginScreen.State { + override fun present(): LoginUiState { val scope = rememberCoroutineScope() var isLoading by rememberRetained { mutableStateOf(false) } - var sideEffect by rememberRetained { mutableStateOf(null) } + var sideEffect by rememberRetained { mutableStateOf(null) } - fun handleEvent(event: LoginScreen.Event) { + fun handleEvent(event: LoginUiEvent) { when (event) { - is LoginScreen.Event.InitSideEffect -> { + is LoginUiEvent.InitSideEffect -> { sideEffect = null } - is LoginScreen.Event.OnKakaoLoginButtonClick -> { + is LoginUiEvent.OnKakaoLoginButtonClick -> { isLoading = true - sideEffect = LoginScreen.SideEffect.KakaoLogin + sideEffect = LoginSideEffect.KakaoLogin } - is LoginScreen.Event.LoginFailure -> { + is LoginUiEvent.LoginFailure -> { isLoading = false - sideEffect = LoginScreen.SideEffect.ShowToast(event.message) + sideEffect = LoginSideEffect.ShowToast(event.message) } - is LoginScreen.Event.Login -> { + is LoginUiEvent.Login -> { scope.launch { try { repository.login(event.accessToken) @@ -54,7 +56,7 @@ class LoginPresenter @AssistedInject constructor( }.onFailure { exception -> exception.message?.let { Logger.e(it) } sideEffect = exception.message?.let { - LoginScreen.SideEffect.ShowToast(it) + LoginSideEffect.ShowToast(it) } } } finally { @@ -65,7 +67,7 @@ class LoginPresenter @AssistedInject constructor( } } - return LoginScreen.State( + return LoginUiState( isLoading = isLoading, sideEffect = sideEffect, eventSink = ::handleEvent, diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginScreen.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginScreen.kt index 1552d132..0acd5ce7 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginScreen.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginScreen.kt @@ -22,39 +22,15 @@ import com.ninecraft.booket.core.designsystem.component.button.ReedButton import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle import com.ninecraft.booket.core.designsystem.component.button.largeButtonStyle import com.ninecraft.booket.core.designsystem.theme.ReedTheme +import com.ninecraft.booket.screens.LoginScreen import com.ninecraft.booket.core.designsystem.theme.White import com.slack.circuit.codegen.annotations.CircuitInject -import com.slack.circuit.runtime.CircuitUiEvent -import com.slack.circuit.runtime.CircuitUiState -import com.slack.circuit.runtime.screen.Screen import dagger.hilt.android.components.ActivityRetainedComponent -import kotlinx.parcelize.Parcelize - -@Parcelize -data object LoginScreen : Screen { - data class State( - val isLoading: Boolean = false, - val sideEffect: SideEffect? = null, - val eventSink: (Event) -> Unit, - ) : CircuitUiState - - sealed interface SideEffect { - data object KakaoLogin : SideEffect - data class ShowToast(val message: String) : SideEffect - } - - sealed interface Event : CircuitUiEvent { - data object InitSideEffect : Event - data object OnKakaoLoginButtonClick : Event - data class Login(val accessToken: String) : Event - data class LoginFailure(val message: String) : Event - } -} @CircuitInject(LoginScreen::class, ActivityRetainedComponent::class) @Composable internal fun Login( - state: LoginScreen.State, + state: LoginUiState, modifier: Modifier = Modifier, ) { HandleLoginSideEffects( @@ -76,7 +52,7 @@ internal fun Login( ) ReedButton( onClick = { - state.eventSink(LoginScreen.Event.OnKakaoLoginButtonClick) + state.eventSink(LoginUiEvent.OnKakaoLoginButtonClick) }, modifier = Modifier .fillMaxWidth() @@ -110,7 +86,7 @@ internal fun Login( private fun LoginPreview() { ReedTheme { Login( - state = LoginScreen.State( + state = LoginUiState( eventSink = {}, ), ) diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUiState.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUiState.kt new file mode 100644 index 00000000..c2495629 --- /dev/null +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUiState.kt @@ -0,0 +1,22 @@ +package com.ninecraft.booket.feature.login + +import com.slack.circuit.runtime.CircuitUiEvent +import com.slack.circuit.runtime.CircuitUiState + +data class LoginUiState( + val isLoading: Boolean = false, + val sideEffect: LoginSideEffect? = null, + val eventSink: (LoginUiEvent) -> Unit, +) : CircuitUiState + +sealed interface LoginSideEffect { + data object KakaoLogin : LoginSideEffect + data class ShowToast(val message: String) : LoginSideEffect +} + +sealed interface LoginUiEvent : CircuitUiEvent { + data object InitSideEffect : LoginUiEvent + data object OnKakaoLoginButtonClick : LoginUiEvent + data class Login(val accessToken: String) : LoginUiEvent + data class LoginFailure(val message: String) : LoginUiEvent +} diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/TermsAgreementPresenter.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/TermsAgreementPresenter.kt similarity index 75% rename from feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/TermsAgreementPresenter.kt rename to feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/TermsAgreementPresenter.kt index 13077e19..406c558f 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/TermsAgreementPresenter.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/TermsAgreementPresenter.kt @@ -1,4 +1,4 @@ -package com.ninecraft.booket.feature.login +package com.ninecraft.booket.feature.termsagreement import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf @@ -6,7 +6,8 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import com.ninecraft.booket.feature.home.HomeScreen +import com.ninecraft.booket.screens.HomeScreen +import com.ninecraft.booket.screens.TermsAgreementScreen import com.slack.circuit.codegen.annotations.CircuitInject import com.slack.circuit.retained.rememberRetained import com.slack.circuit.runtime.Navigator @@ -20,10 +21,10 @@ import kotlinx.collections.immutable.toPersistentList class TermsAgreementPresenter @AssistedInject constructor( @Assisted private val navigator: Navigator, -) : Presenter { +) : Presenter { @Composable - override fun present(): TermsAgreementScreen.State { + override fun present(): TermsAgreementUiState { var agreedTerms by rememberRetained { mutableStateOf(persistentListOf(false, false, false)) } @@ -34,32 +35,32 @@ class TermsAgreementPresenter @AssistedInject constructor( } } - fun handleEvent(event: TermsAgreementScreen.Event) { + fun handleEvent(event: TermsAgreementUiEvent) { when (event) { - is TermsAgreementScreen.Event.OnAllTermsAgreedClick -> { + is TermsAgreementUiEvent.OnAllTermsAgreedClick -> { val toggleAgreed = !isAllAgreed agreedTerms = agreedTerms.map { toggleAgreed }.toPersistentList() } - is TermsAgreementScreen.Event.OnTermItemClick -> { + is TermsAgreementUiEvent.OnTermItemClick -> { agreedTerms = agreedTerms.set(event.index, !agreedTerms[event.index]) } - is TermsAgreementScreen.Event.OnBackClick -> { + is TermsAgreementUiEvent.OnBackClick -> { navigator.pop() } - is TermsAgreementScreen.Event.OnTermDetailClick -> { + is TermsAgreementUiEvent.OnTermDetailClick -> { // TODO: 웹뷰 화면으로 이동 } - is TermsAgreementScreen.Event.OnStartButtonClick -> { + is TermsAgreementUiEvent.OnStartButtonClick -> { navigator.resetRoot(HomeScreen) } } } - return TermsAgreementScreen.State( + return TermsAgreementUiState( isAllAgreed = isAllAgreed, agreedTerms = agreedTerms, eventSink = ::handleEvent, diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/TermsAgreementScreen.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/TermsAgreementScreen.kt similarity index 83% rename from feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/TermsAgreementScreen.kt rename to feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/TermsAgreementScreen.kt index c1932e26..7c47cfcf 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/TermsAgreementScreen.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/TermsAgreementScreen.kt @@ -1,4 +1,4 @@ -package com.ninecraft.booket.feature.login +package com.ninecraft.booket.feature.termsagreement import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -32,36 +32,16 @@ import com.ninecraft.booket.core.designsystem.component.checkbox.SquareCheckBox import com.ninecraft.booket.core.designsystem.component.checkbox.TickOnlyCheckBox import com.ninecraft.booket.core.designsystem.theme.ReedTheme import com.ninecraft.booket.core.designsystem.theme.White +import com.ninecraft.booket.feature.login.R +import com.ninecraft.booket.screens.TermsAgreementScreen import com.slack.circuit.codegen.annotations.CircuitInject -import com.slack.circuit.runtime.CircuitUiEvent -import com.slack.circuit.runtime.CircuitUiState -import com.slack.circuit.runtime.screen.Screen import dagger.hilt.android.components.ActivityRetainedComponent -import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -import kotlinx.parcelize.Parcelize - -@Parcelize -data object TermsAgreementScreen : Screen { - data class State( - val isAllAgreed: Boolean, - val agreedTerms: ImmutableList, - val eventSink: (Event) -> Unit, - ) : CircuitUiState - - sealed interface Event : CircuitUiEvent { - data object OnAllTermsAgreedClick : Event - data class OnTermItemClick(val index: Int) : Event - data object OnBackClick : Event - data class OnTermDetailClick(val url: String) : Event - data object OnStartButtonClick : Event - } -} @CircuitInject(TermsAgreementScreen::class, ActivityRetainedComponent::class) @Composable internal fun TermsAgreement( - state: TermsAgreementScreen.State, + state: TermsAgreementUiState, modifier: Modifier = Modifier, ) { Column( @@ -71,7 +51,7 @@ internal fun TermsAgreement( ) { ReedBackTopAppBar( onNavigateBack = { - state.eventSink(TermsAgreementScreen.Event.OnBackClick) + state.eventSink(TermsAgreementUiEvent.OnBackClick) }, ) Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) @@ -103,7 +83,7 @@ internal fun TermsAgreement( SquareCheckBox( checked = state.isAllAgreed, onCheckedChange = { - state.eventSink(TermsAgreementScreen.Event.OnAllTermsAgreedClick) + state.eventSink(TermsAgreementUiEvent.OnAllTermsAgreedClick) }, ) Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing4)) @@ -122,17 +102,17 @@ internal fun TermsAgreement( title = title, checked = state.agreedTerms[index], onCheckClick = { - state.eventSink(TermsAgreementScreen.Event.OnTermItemClick(index)) + state.eventSink(TermsAgreementUiEvent.OnTermItemClick(index)) }, onDetailClick = { - state.eventSink(TermsAgreementScreen.Event.OnTermDetailClick("")) + state.eventSink(TermsAgreementUiEvent.OnTermDetailClick("")) }, ) } } ReedButton( onClick = { - state.eventSink(TermsAgreementScreen.Event.OnStartButtonClick) + state.eventSink(TermsAgreementUiEvent.OnStartButtonClick) }, modifier = Modifier .fillMaxWidth() @@ -195,7 +175,7 @@ private fun TermItem( private fun TermsAgreementPreview() { ReedTheme { TermsAgreement( - state = TermsAgreementScreen.State( + state = TermsAgreementUiState( isAllAgreed = false, agreedTerms = persistentListOf(false, false, false), eventSink = {}, diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/TermsAgreementUiState.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/TermsAgreementUiState.kt new file mode 100644 index 00000000..5e19d9ec --- /dev/null +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/TermsAgreementUiState.kt @@ -0,0 +1,19 @@ +package com.ninecraft.booket.feature.termsagreement + +import com.slack.circuit.runtime.CircuitUiEvent +import com.slack.circuit.runtime.CircuitUiState +import kotlinx.collections.immutable.ImmutableList + +data class TermsAgreementUiState( + val isAllAgreed: Boolean, + val agreedTerms: ImmutableList, + val eventSink: (TermsAgreementUiEvent) -> Unit, +) : CircuitUiState + +sealed interface TermsAgreementUiEvent : CircuitUiEvent { + data object OnAllTermsAgreedClick : TermsAgreementUiEvent + data class OnTermItemClick(val index: Int) : TermsAgreementUiEvent + data object OnBackClick : TermsAgreementUiEvent + data class OnTermDetailClick(val url: String) : TermsAgreementUiEvent + data object OnStartButtonClick : TermsAgreementUiEvent +} diff --git a/feature/main/build.gradle.kts b/feature/main/build.gradle.kts index b955ed1a..491b84ee 100644 --- a/feature/main/build.gradle.kts +++ b/feature/main/build.gradle.kts @@ -15,11 +15,6 @@ ksp { dependencies { implementations( - projects.feature.home, - projects.feature.library, - projects.feature.login, - projects.feature.search, - libs.kotlinx.collections.immutable, libs.androidx.activity.compose, diff --git a/feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/MainActivity.kt b/feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/MainActivity.kt index 1efdc0c4..e9964a3d 100644 --- a/feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/MainActivity.kt +++ b/feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/MainActivity.kt @@ -9,8 +9,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.ui.Modifier import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen -import com.ninecraft.booket.feature.login.LoginScreen import com.ninecraft.booket.feature.main.component.MainBottomBar +import com.ninecraft.booket.screens.LoginScreen import com.slack.circuit.backstack.rememberSaveableBackStack import com.slack.circuit.foundation.Circuit import com.slack.circuit.foundation.CircuitCompositionLocals diff --git a/feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/component/MainBottomBar.kt b/feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/component/MainBottomBar.kt index bb3cdb70..ad743e97 100644 --- a/feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/component/MainBottomBar.kt +++ b/feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/component/MainBottomBar.kt @@ -34,9 +34,9 @@ import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import com.ninecraft.booket.core.designsystem.ComponentPreview import com.ninecraft.booket.core.designsystem.theme.ReedTheme -import com.ninecraft.booket.feature.home.HomeScreen -import com.ninecraft.booket.feature.library.LibraryScreen -import com.ninecraft.booket.feature.search.SearchScreen +import com.ninecraft.booket.screens.HomeScreen +import com.ninecraft.booket.screens.LibraryScreen +import com.ninecraft.booket.screens.SearchScreen import com.slack.circuit.backstack.SaveableBackStack import com.slack.circuit.runtime.Navigator import com.slack.circuit.runtime.popUntil diff --git a/feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/component/MainTab.kt b/feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/component/MainTab.kt index d6190034..5e46033c 100644 --- a/feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/component/MainTab.kt +++ b/feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/component/MainTab.kt @@ -2,10 +2,10 @@ package com.ninecraft.booket.feature.main.component import androidx.annotation.DrawableRes import androidx.annotation.StringRes -import com.ninecraft.booket.feature.home.HomeScreen -import com.ninecraft.booket.feature.library.LibraryScreen import com.ninecraft.booket.feature.main.R -import com.ninecraft.booket.feature.search.SearchScreen +import com.ninecraft.booket.screens.HomeScreen +import com.ninecraft.booket.screens.LibraryScreen +import com.ninecraft.booket.screens.SearchScreen import com.slack.circuit.runtime.screen.Screen internal enum class MainTab( diff --git a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/SearchPresenter.kt b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/SearchPresenter.kt index be2cc2e1..47364571 100644 --- a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/SearchPresenter.kt +++ b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/SearchPresenter.kt @@ -2,6 +2,7 @@ package com.ninecraft.booket.feature.search import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope +import com.ninecraft.booket.screens.SearchScreen import com.slack.circuit.codegen.annotations.CircuitInject import com.slack.circuit.runtime.Navigator import com.slack.circuit.runtime.presenter.Presenter @@ -13,13 +14,13 @@ import dagger.hilt.android.components.ActivityRetainedComponent @Suppress("unused") class SearchPresenter @AssistedInject constructor( @Assisted private val navigator: Navigator, -) : Presenter { +) : Presenter { @Composable - override fun present(): SearchScreen.State { + override fun present(): SearchUiState { val scope = rememberCoroutineScope() - return SearchScreen.State {} + return SearchUiState {} } @CircuitInject(SearchScreen::class, ActivityRetainedComponent::class) diff --git a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/SearchScreen.kt b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/SearchScreen.kt index a97ac65e..2d1681d9 100644 --- a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/SearchScreen.kt +++ b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/SearchScreen.kt @@ -9,26 +9,14 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.ninecraft.booket.core.designsystem.DevicePreview import com.ninecraft.booket.core.designsystem.theme.ReedTheme +import com.ninecraft.booket.screens.SearchScreen import com.slack.circuit.codegen.annotations.CircuitInject -import com.slack.circuit.runtime.CircuitUiEvent -import com.slack.circuit.runtime.CircuitUiState -import com.slack.circuit.runtime.screen.Screen import dagger.hilt.android.components.ActivityRetainedComponent -import kotlinx.parcelize.Parcelize - -@Parcelize -data object SearchScreen : Screen { - data class State( - val eventSink: (Event) -> Unit, - ) : CircuitUiState - - sealed interface Event : CircuitUiEvent -} @CircuitInject(SearchScreen::class, ActivityRetainedComponent::class) @Composable internal fun Search( - state: SearchScreen.State, + state: SearchUiState, modifier: Modifier = Modifier, ) { Column( @@ -46,7 +34,7 @@ internal fun Search( @Suppress("unused") @Composable internal fun SearchContent( - state: SearchScreen.State, + state: SearchUiState, modifier: Modifier = Modifier, ) { Text(text = "도서 검색") @@ -57,7 +45,7 @@ internal fun SearchContent( private fun SearchPreview() { ReedTheme { Search( - state = SearchScreen.State( + state = SearchUiState( eventSink = {}, ), ) diff --git a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/SearchUiState.kt b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/SearchUiState.kt new file mode 100644 index 00000000..678358a2 --- /dev/null +++ b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/SearchUiState.kt @@ -0,0 +1,10 @@ +package com.ninecraft.booket.feature.search + +import com.slack.circuit.runtime.CircuitUiEvent +import com.slack.circuit.runtime.CircuitUiState + +data class SearchUiState( + val eventSink: (SearchUiEvent) -> Unit, +) : CircuitUiState + +sealed interface SearchUiEvent : CircuitUiEvent diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4d88e53d..f84e903f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -104,6 +104,7 @@ kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "kotlinx-collections-immutable" } circuit-foundation = { group = "com.slack.circuit", name = "circuit-foundation", version.ref = "circuit" } +circuit-runtime = { group = "com.slack.circuit", name = "circuit-runtime", version.ref = "circuit" } circuitx-android = { group = "com.slack.circuit", name = "circuitx-android", version.ref = "circuit" } circuitx-overlays = { group = "com.slack.circuit", name = "circuitx-overlays", version.ref = "circuit" } circuitx-gesture-navigation = { group = "com.slack.circuit", name = "circuitx-gesture-navigation", version.ref = "circuit" } diff --git a/gradle/projectDependencyGraph.gradle b/gradle/projectDependencyGraph.gradle new file mode 100644 index 00000000..a0def4f1 --- /dev/null +++ b/gradle/projectDependencyGraph.gradle @@ -0,0 +1,131 @@ +tasks.register('projectDependencyGraph') { + doLast { + def dotFileName = 'project.dot' + def dot = new File(rootProject.rootDir, dotFileName) + dot.parentFile.mkdirs() + dot.delete() + + dot << 'digraph {\n' + dot << " graph [label=\"${rootProject.name}\\n \",labelloc=t,fontsize=30,ranksep=1.4];\n" + dot << ' node [style=filled, fillcolor="#bbbbbb"];\n' + dot << ' rankdir=TB;\n' + + def rootProjects = new LinkedHashSet() + def queue = [rootProject] + while (!queue.isEmpty()) { + def project = queue.remove(0) + rootProjects.add(project) + queue.addAll(project.childProjects.values()) + } + + def projects = new LinkedHashSet() + def dependencies = new LinkedHashMap, List>() + def multiplatformProjects = [] + def jsProjects = [] + def androidProjects = [] + def androidLibraryProjects = [] + def androidDynamicFeatureProjects = [] + def javaProjects = [] + + queue = [rootProject] + while (!queue.isEmpty()) { + def project = queue.remove(0) + queue.addAll(project.childProjects.values()) + + if (project.plugins.hasPlugin('org.jetbrains.kotlin.multiplatform')) { + multiplatformProjects.add(project) + } + if (project.plugins.hasPlugin('kotlin2js')) { + jsProjects.add(project) + } + if (project.plugins.hasPlugin('com.android.application')) { + androidProjects.add(project) + } + if (project.plugins.hasPlugin('com.android.library')) { + androidLibraryProjects.add(project) + } + if (project.plugins.hasPlugin('com.android.dynamic-feature')) { + androidDynamicFeatureProjects.add(project) + } + if (project.plugins.hasPlugin('java-library') || project.plugins.hasPlugin('java')) { + javaProjects.add(project) + } + + project.configurations.configureEach { config -> + if (config.name.toLowerCase().contains("test")) return + config.dependencies + .withType(ProjectDependency) + .collect { it.dependencyProject } + .each { dependency -> + projects.add(project) + projects.add(dependency) + rootProjects.remove(dependency) + + def graphKey = new Tuple2(project, dependency) + def traits = dependencies.computeIfAbsent(graphKey) { new ArrayList() } + + if (config.name.toLowerCase().endsWith('implementation')) { + traits.add('style=dotted') + } + } + } + } + + projects = projects.sort { it.path } + + dot << '\n # Projects\n\n' + for (project in projects) { + def traits = [] + + if (rootProjects.contains(project)) { + traits.add('shape=box') + } + + if (multiplatformProjects.contains(project)) { + traits.add('fillcolor="#ffd2b3"') + } else if (jsProjects.contains(project)) { + traits.add('fillcolor="#ffffba"') + } else if (androidProjects.contains(project)) { + traits.add('fillcolor="#baffc9"') + } else if (androidLibraryProjects.contains(project)) { + traits.add('fillcolor="#81D4FA"') + } else if (androidDynamicFeatureProjects.contains(project)) { + traits.add('fillcolor="#c9baff"') + } else if (javaProjects.contains(project)) { + traits.add('fillcolor="#ffb3ba"') + } else { + traits.add('fillcolor="#eeeeee"') + } + + dot << " \"${project.path}\" [${traits.join(", ")}];\n" + } + + dot << '\n {rank = same;' + for (project in projects) { + if (rootProjects.contains(project)) { + dot << " \"${project.path}\";" + } + } + dot << '}\n' + + dot << '\n # Dependencies\n\n' + dependencies.forEach { key, traits -> + dot << " \"${key.first.path}\" -> \"${key.second.path}\"" + if (!traits.isEmpty()) { + dot << " [${traits.join(", ")}]" + } + dot << '\n' + } + + dot << '}\n' + + def p = "dot -Tpng -O ${dotFileName}".execute([], dot.parentFile) + p.waitFor() + if (p.exitValue() != 0) { + throw new RuntimeException(p.errorStream.text) + } + dot.delete() + + println("Project module dependency graph created at ${dot.absolutePath}.png") + } +} diff --git a/screens/.gitignore b/screens/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/screens/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/screens/build.gradle.kts b/screens/build.gradle.kts new file mode 100644 index 00000000..78c238e7 --- /dev/null +++ b/screens/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + alias(libs.plugins.booket.android.library) + alias(libs.plugins.kotlin.parcelize) +} + +android { + namespace = "com.ninecraft.booket.screens" +} + +dependencies { + implementation(projects.core.model) + api(libs.circuit.runtime) +} diff --git a/screens/src/main/kotlin/com/ninecraft/booket/screens/Screens.kt b/screens/src/main/kotlin/com/ninecraft/booket/screens/Screens.kt new file mode 100644 index 00000000..450a391d --- /dev/null +++ b/screens/src/main/kotlin/com/ninecraft/booket/screens/Screens.kt @@ -0,0 +1,23 @@ +package com.ninecraft.booket.screens + +import com.slack.circuit.runtime.screen.Screen +import kotlinx.parcelize.Parcelize + +abstract class ReedScreen(val name: String) : Screen { + override fun toString(): String = name +} + +@Parcelize +data object HomeScreen : ReedScreen(name = "Home()") + +@Parcelize +data object LibraryScreen : ReedScreen(name = "Library()") + +@Parcelize +data object LoginScreen : ReedScreen(name = "Login()") + +@Parcelize +data object SearchScreen : ReedScreen(name = "Search()") + +@Parcelize +data object TermsAgreementScreen : ReedScreen(name = "TermsAgreement()") diff --git a/settings.gradle.kts b/settings.gradle.kts index b4e2e3e0..ff8a48c2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -39,4 +39,6 @@ include( ":feature:login", ":feature:main", ":feature:search", + + ":screens", )