diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8568fa9e..2f97c865 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -26,13 +26,17 @@ android { buildTypes { release { - isMinifyEnabled = false + getByName("debug") + isMinifyEnabled = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } + buildFeatures { + buildConfig = true + } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 @@ -57,4 +61,5 @@ dependencies { implementation(project(":feature:main")) implementation(project(":feature:login")) implementation(project(":core:designsystem")) -} \ No newline at end of file + implementation(project(":core:data")) +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 33341792..1c00b2cc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + - + android:networkSecurityConfig="@xml/network_security_config" + tools:targetApi="31"/> \ No newline at end of file diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 00000000..872512f7 --- /dev/null +++ b/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,6 @@ + + + + 223.130.130.31 + + \ No newline at end of file diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index c6fba51a..e9cdff2a 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -45,4 +45,8 @@ dependencies { ksp(libs.hilt.compiler) implementation(libs.hilt.android) + + implementation(project(":core:domain")) + implementation(project(":core:model")) + implementation(project(":core:network")) } \ No newline at end of file diff --git a/core/data/src/androidTest/java/com/goalpanzi/mission_mate/core/data/ExampleInstrumentedTest.kt b/core/data/src/androidTest/java/com/goalpanzi/mission_mate/core/data/ExampleInstrumentedTest.kt deleted file mode 100644 index 893f4c99..00000000 --- a/core/data/src/androidTest/java/com/goalpanzi/mission_mate/core/data/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.goalpanzi.mission_mate.core.data - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.goalpanzi.mission_mate.core.data.test", appContext.packageName) - } -} \ No newline at end of file diff --git a/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/Data.kt b/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/Data.kt deleted file mode 100644 index 4b970b65..00000000 --- a/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/Data.kt +++ /dev/null @@ -1,4 +0,0 @@ -package com.goalpanzi.mission_mate.core.data - -class Data { -} \ No newline at end of file diff --git a/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/di/DataModule.kt b/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/di/DataModule.kt new file mode 100644 index 00000000..21696f28 --- /dev/null +++ b/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/di/DataModule.kt @@ -0,0 +1,16 @@ +package com.goalpanzi.mission_mate.core.data.di + +import com.goalpanzi.mission_mate.core.data.repository.LoginRepositoryImpl +import com.goalpanzi.mission_mate.core.domain.repository.LoginRepository +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +internal abstract class DataModule { + + @Binds + abstract fun bindLoginRepository(impl: LoginRepositoryImpl): LoginRepository +} \ No newline at end of file diff --git a/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/di/DispatchersModule.kt b/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/di/DispatchersModule.kt new file mode 100644 index 00000000..2b129a1b --- /dev/null +++ b/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/di/DispatchersModule.kt @@ -0,0 +1,27 @@ +package com.goalpanzi.mission_mate.core.data.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +annotation class Dispatcher(val dispatchers: MissionMateDispatcher) + +enum class MissionMateDispatcher { + IO +} + +@Module +@InstallIn(SingletonComponent::class) +interface DispatchersModule { + + @Provides + @Dispatcher(MissionMateDispatcher.IO) + fun providesIODispatcher(): CoroutineDispatcher = Dispatchers.IO + +} \ No newline at end of file diff --git a/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/repository/LoginRepositoryImpl.kt b/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/repository/LoginRepositoryImpl.kt new file mode 100644 index 00000000..5b257823 --- /dev/null +++ b/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/repository/LoginRepositoryImpl.kt @@ -0,0 +1,18 @@ +package com.goalpanzi.mission_mate.core.data.repository + +import com.goalpanzi.mission_mate.core.domain.repository.LoginRepository +import com.goalpanzi.mission_mate.core.network.service.LoginService +import com.luckyoct.model.GoogleLogin +import com.luckyoct.model.request.GoogleLoginRequest +import javax.inject.Inject + +class LoginRepositoryImpl @Inject constructor( + private val loginService: LoginService +): LoginRepository { + + override suspend fun requestGoogleLogin(token: String, email: String): GoogleLogin { + val request = GoogleLoginRequest(identityToken = token, email = email) + val response = loginService.requestGoogleLogin(request) + return response + } +} \ No newline at end of file diff --git a/core/datastore/src/androidTest/java/com/goalpanzi/mission_mate/core/datastore/ExampleInstrumentedTest.kt b/core/datastore/src/androidTest/java/com/goalpanzi/mission_mate/core/datastore/ExampleInstrumentedTest.kt deleted file mode 100644 index b6e103ad..00000000 --- a/core/datastore/src/androidTest/java/com/goalpanzi/mission_mate/core/datastore/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.goalpanzi.mission_mate.core.datastore - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.goalpanzi.mission_mate.core.datastore.test", appContext.packageName) - } -} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/theme/Color.kt b/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/theme/Color.kt index 10bade95..5068d8ef 100644 --- a/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/theme/Color.kt +++ b/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/theme/Color.kt @@ -8,4 +8,6 @@ val Pink80 = Color(0xFFEFB8C8) val Purple40 = Color(0xFF6650a4) val PurpleGrey40 = Color(0xFF625b71) -val Pink40 = Color(0xFF7D5260) \ No newline at end of file +val Pink40 = Color(0xFF7D5260) + +val Orange01 = Color(0xFFF5EDEA) \ No newline at end of file diff --git a/core/domain/build.gradle.kts b/core/domain/build.gradle.kts index d3dd2cc9..e5538f89 100644 --- a/core/domain/build.gradle.kts +++ b/core/domain/build.gradle.kts @@ -45,4 +45,6 @@ dependencies { ksp(libs.hilt.compiler) implementation(libs.hilt.android) + + implementation(project(":core:model")) } \ No newline at end of file diff --git a/core/domain/src/androidTest/java/com/goalpanzi/mission_mate/core/domain/ExampleInstrumentedTest.kt b/core/domain/src/androidTest/java/com/goalpanzi/mission_mate/core/domain/ExampleInstrumentedTest.kt deleted file mode 100644 index ff04cf42..00000000 --- a/core/domain/src/androidTest/java/com/goalpanzi/mission_mate/core/domain/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.goalpanzi.mission_mate.core.domain - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.goalpanzi.mission_mate.core.domain.test", appContext.packageName) - } -} \ No newline at end of file diff --git a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/Domain.kt b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/Domain.kt deleted file mode 100644 index 17c80efa..00000000 --- a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/Domain.kt +++ /dev/null @@ -1,4 +0,0 @@ -package com.goalpanzi.mission_mate.core.domain - -class Domain { -} \ No newline at end of file diff --git a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/repository/LoginRepository.kt b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/repository/LoginRepository.kt new file mode 100644 index 00000000..dfe6f1fc --- /dev/null +++ b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/repository/LoginRepository.kt @@ -0,0 +1,7 @@ +package com.goalpanzi.mission_mate.core.domain.repository + +import com.luckyoct.model.GoogleLogin + +interface LoginRepository { + suspend fun requestGoogleLogin(token: String, email: String): GoogleLogin +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/LoginUseCase.kt b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/LoginUseCase.kt new file mode 100644 index 00000000..5ff15bce --- /dev/null +++ b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/LoginUseCase.kt @@ -0,0 +1,13 @@ +package com.goalpanzi.mission_mate.core.domain.usecase + +import com.goalpanzi.mission_mate.core.domain.repository.LoginRepository +import com.luckyoct.model.GoogleLogin +import javax.inject.Inject + +class LoginUseCase @Inject constructor( + private val loginRepository: LoginRepository +) { + + suspend fun requestGoogleLogin(token: String, email: String): GoogleLogin = loginRepository.requestGoogleLogin(token, email) + +} \ No newline at end of file diff --git a/core/model/.gitignore b/core/model/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/model/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/model/build.gradle.kts b/core/model/build.gradle.kts new file mode 100644 index 00000000..0c2067bb --- /dev/null +++ b/core/model/build.gradle.kts @@ -0,0 +1,42 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.jetbrains.kotlin.android) + alias(libs.plugins.kotlin.plugin.serialization) +} + +android { + namespace = "com.luckyoct.model" + compileSdk = 34 + + defaultConfig { + minSdk = 24 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + + implementation(libs.kotlin.serialization.json) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) +} \ No newline at end of file diff --git a/core/model/consumer-rules.pro b/core/model/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/core/model/proguard-rules.pro b/core/model/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/core/model/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/core/model/src/main/AndroidManifest.xml b/core/model/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/core/model/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/core/model/src/main/java/com/luckyoct/model/GoogleLogin.kt b/core/model/src/main/java/com/luckyoct/model/GoogleLogin.kt new file mode 100644 index 00000000..406c5eca --- /dev/null +++ b/core/model/src/main/java/com/luckyoct/model/GoogleLogin.kt @@ -0,0 +1,10 @@ +package com.luckyoct.model + +import kotlinx.serialization.Serializable + +@Serializable +data class GoogleLogin( + val accessToken: String, + val refreshToken: String, + val isProfileSet: Boolean +) diff --git a/core/model/src/main/java/com/luckyoct/model/request/GoogleLoginRequest.kt b/core/model/src/main/java/com/luckyoct/model/request/GoogleLoginRequest.kt new file mode 100644 index 00000000..c844ac49 --- /dev/null +++ b/core/model/src/main/java/com/luckyoct/model/request/GoogleLoginRequest.kt @@ -0,0 +1,9 @@ +package com.luckyoct.model.request + +import kotlinx.serialization.Serializable + +@Serializable +data class GoogleLoginRequest( + val identityToken: String, + val email: String +) diff --git a/core/model/src/test/java/com/luckyoct/model/ExampleUnitTest.kt b/core/model/src/test/java/com/luckyoct/model/ExampleUnitTest.kt new file mode 100644 index 00000000..dfdf2fb7 --- /dev/null +++ b/core/model/src/test/java/com/luckyoct/model/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.luckyoct.model + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/core/navigation/src/androidTest/java/com/goalpanzi/mission_mate/core/navigation/ExampleInstrumentedTest.kt b/core/navigation/src/androidTest/java/com/goalpanzi/mission_mate/core/navigation/ExampleInstrumentedTest.kt deleted file mode 100644 index 3939f16d..00000000 --- a/core/navigation/src/androidTest/java/com/goalpanzi/mission_mate/core/navigation/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.goalpanzi.mission_mate.core.navigation - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleInstrumentedTest { - -} \ No newline at end of file diff --git a/core/navigation/src/main/java/com/goalpanzi/mission_mate/core/navigation/Navigation.kt b/core/navigation/src/main/java/com/goalpanzi/mission_mate/core/navigation/Navigation.kt deleted file mode 100644 index f95186bb..00000000 --- a/core/navigation/src/main/java/com/goalpanzi/mission_mate/core/navigation/Navigation.kt +++ /dev/null @@ -1,4 +0,0 @@ -package com.goalpanzi.mission_mate.core.navigation - -class Navigation { -} \ No newline at end of file diff --git a/core/navigation/src/main/java/com/goalpanzi/mission_mate/core/navigation/RouteModel.kt b/core/navigation/src/main/java/com/goalpanzi/mission_mate/core/navigation/RouteModel.kt new file mode 100644 index 00000000..d0ad89be --- /dev/null +++ b/core/navigation/src/main/java/com/goalpanzi/mission_mate/core/navigation/RouteModel.kt @@ -0,0 +1,8 @@ +package com.goalpanzi.mission_mate.core.navigation + +import kotlinx.serialization.Serializable + +sealed interface RouteModel { + @Serializable + data object Login : RouteModel +} \ No newline at end of file diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts index 99e10005..9b0226f3 100644 --- a/core/network/build.gradle.kts +++ b/core/network/build.gradle.kts @@ -20,6 +20,9 @@ android { } buildTypes { + debug { + buildConfigField("String", "BASE_URL", "\"http://223.130.130.31:8080\"") + } release { isMinifyEnabled = false proguardFiles( @@ -28,6 +31,9 @@ android { ) } } + buildFeatures { + buildConfig = true + } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 @@ -48,4 +54,6 @@ dependencies { ksp(libs.hilt.compiler) implementation(libs.hilt.android) + + implementation(project(":core:model")) } \ No newline at end of file diff --git a/core/network/src/androidTest/java/com/goalpanzi/mission_mate/core/network/ExampleInstrumentedTest.kt b/core/network/src/androidTest/java/com/goalpanzi/mission_mate/core/network/ExampleInstrumentedTest.kt deleted file mode 100644 index 089f385d..00000000 --- a/core/network/src/androidTest/java/com/goalpanzi/mission_mate/core/network/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.goalpanzi.mission_mate.core.network - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.goalpanzi.mission_mate.core.network.test", appContext.packageName) - } -} \ No newline at end of file diff --git a/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/Network.kt b/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/Network.kt deleted file mode 100644 index 4112733d..00000000 --- a/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/Network.kt +++ /dev/null @@ -1,4 +0,0 @@ -package com.goalpanzi.mission_mate.core.network - -class Network { -} \ No newline at end of file diff --git a/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/di/NetworkModule.kt b/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/di/NetworkModule.kt new file mode 100644 index 00000000..fa3284ae --- /dev/null +++ b/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/di/NetworkModule.kt @@ -0,0 +1,75 @@ +package com.goalpanzi.mission_mate.core.network.di + +import com.goalpanzi.mission_mate.core.network.BuildConfig +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Converter +import retrofit2.Retrofit +import retrofit2.converter.kotlinx.serialization.asConverterFactory +import java.security.KeyStore +import javax.inject.Singleton +import javax.net.ssl.SSLContext +import javax.net.ssl.TrustManagerFactory +import javax.net.ssl.X509TrustManager + +@Module +@InstallIn(SingletonComponent::class) +internal object NetworkModule { + + @Provides + @Singleton + fun provideRequestHttpLoggingInterceptor(): HttpLoggingInterceptor { + return HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY } + } + + @Provides + @Singleton + fun provideOkhttpClient( + httpLoggingInterceptor: HttpLoggingInterceptor + ): OkHttpClient { + // TLS 대응 + val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) + trustManagerFactory.init(null as KeyStore?) + val trustManager = trustManagerFactory.trustManagers[0] as X509TrustManager + val sslContext = SSLContext.getInstance("TLS") + sslContext.init(null, arrayOf(trustManager), java.security.SecureRandom()) + + return OkHttpClient.Builder() + .sslSocketFactory(sslContext.socketFactory, trustManager) + .addInterceptor(httpLoggingInterceptor) + .build() + } + + @Provides + @Singleton + fun provideJson(): Json = Json { + ignoreUnknownKeys = true + } + + @Provides + @Singleton + fun provideConverterFactory( + json: Json, + ): Converter.Factory { + return json.asConverterFactory("application/json".toMediaType()) + } + + @Provides + @Singleton + fun provideRetrofit( + okHttpClient: OkHttpClient, + converterFactory: Converter.Factory + ) : Retrofit { + return Retrofit.Builder() + .client(okHttpClient) + .baseUrl(BuildConfig.BASE_URL) + .addConverterFactory(converterFactory) + .build() + } +} \ No newline at end of file diff --git a/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/di/ServiceModule.kt b/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/di/ServiceModule.kt new file mode 100644 index 00000000..0d248b31 --- /dev/null +++ b/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/di/ServiceModule.kt @@ -0,0 +1,20 @@ +package com.goalpanzi.mission_mate.core.network.di + +import com.goalpanzi.mission_mate.core.network.service.LoginService +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import retrofit2.Retrofit +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object ServiceModule { + + @Provides + @Singleton + fun provideLoginService(retrofit: Retrofit): LoginService { + return retrofit.create(LoginService::class.java) + } +} \ No newline at end of file diff --git a/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/service/LoginService.kt b/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/service/LoginService.kt new file mode 100644 index 00000000..79c07b5c --- /dev/null +++ b/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/service/LoginService.kt @@ -0,0 +1,14 @@ +package com.goalpanzi.mission_mate.core.network.service + +import com.luckyoct.model.GoogleLogin +import com.luckyoct.model.request.GoogleLoginRequest +import retrofit2.http.Body +import retrofit2.http.POST + +interface LoginService { + + @POST("/api/auth/login/google") + suspend fun requestGoogleLogin( + @Body request: GoogleLoginRequest + ): GoogleLogin +} \ No newline at end of file diff --git a/feature/login/build.gradle.kts b/feature/login/build.gradle.kts index e0a9e624..fb862b5f 100644 --- a/feature/login/build.gradle.kts +++ b/feature/login/build.gradle.kts @@ -1,3 +1,4 @@ +import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { @@ -23,8 +24,12 @@ android { } buildTypes { + debug { + buildConfigField("String", "CREDENTIAL_WEB_CLIENT_ID", getCredentialClientId()) + } release { - isMinifyEnabled = false + getByName("debug") + isMinifyEnabled = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" @@ -42,6 +47,7 @@ android { } buildFeatures { compose = true + buildConfig = true } composeCompiler { enableStrongSkippingMode = true @@ -72,4 +78,15 @@ dependencies { ksp(libs.hilt.compiler) implementation(project(":core:designsystem")) -} \ No newline at end of file + implementation(project(":core:navigation")) + implementation(project(":core:domain")) + implementation(project(":core:model")) + + implementation(libs.credentials) + implementation(libs.credentials.auth) + implementation(libs.google.id) +} + +fun getCredentialClientId(): String { + return gradleLocalProperties(rootDir, providers).getProperty("CREDENTIAL_WEB_CLIENT_ID") ?: "" +} diff --git a/feature/login/proguard-rules.pro b/feature/login/proguard-rules.pro index 481bb434..71dd361f 100644 --- a/feature/login/proguard-rules.pro +++ b/feature/login/proguard-rules.pro @@ -18,4 +18,8 @@ # If you keep the line number information, uncomment this to # hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file +#-renamesourcefileattribute SourceFile +-if class androidx.credentials.CredentialManager +-keep class androidx.credentials.playservices.** { + *; +} diff --git a/feature/login/src/main/java/com/goalpanzi/mission_mate/feature/login/LoginActivity.kt b/feature/login/src/main/java/com/goalpanzi/mission_mate/feature/login/LoginActivity.kt deleted file mode 100644 index 6ee3bbcf..00000000 --- a/feature/login/src/main/java/com/goalpanzi/mission_mate/feature/login/LoginActivity.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.goalpanzi.mission_mate.feature.login - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.activity.enableEdgeToEdge -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import com.goalpanzi.mission_mate.core.designsystem.theme.MissionmateTheme -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class LoginActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enableEdgeToEdge() - setContent { - MissionmateTheme { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> - Greeting( - name = "Android", - modifier = Modifier.padding(innerPadding) - ) - } - } - } - } -} - -@Composable -fun Greeting(name: String, modifier: Modifier = Modifier) { - Text( - text = "Hello $name!", - modifier = modifier - ) -} - -@Preview(showBackground = true) -@Composable -fun GreetingPreview() { - MissionmateTheme { - Greeting("Android") - } -} \ No newline at end of file diff --git a/feature/login/src/main/java/com/goalpanzi/mission_mate/feature/login/LoginNavigation.kt b/feature/login/src/main/java/com/goalpanzi/mission_mate/feature/login/LoginNavigation.kt new file mode 100644 index 00000000..dd559be8 --- /dev/null +++ b/feature/login/src/main/java/com/goalpanzi/mission_mate/feature/login/LoginNavigation.kt @@ -0,0 +1,20 @@ +package com.goalpanzi.mission_mate.feature.login + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import com.goalpanzi.mission_mate.core.navigation.RouteModel + +fun NavController.navigateToLogin() { + this.navigate(RouteModel.Login) +} + +fun NavGraphBuilder.loginNavGraph( + onBackClick: () -> Unit +) { + composable { + LoginRoute( + onBackClick = onBackClick + ) + } +} \ No newline at end of file diff --git a/feature/login/src/main/java/com/goalpanzi/mission_mate/feature/login/LoginScreen.kt b/feature/login/src/main/java/com/goalpanzi/mission_mate/feature/login/LoginScreen.kt new file mode 100644 index 00000000..a487f0e8 --- /dev/null +++ b/feature/login/src/main/java/com/goalpanzi/mission_mate/feature/login/LoginScreen.kt @@ -0,0 +1,99 @@ +package com.goalpanzi.mission_mate.feature.login + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography +import com.goalpanzi.mission_mate.core.designsystem.theme.Orange01 + +@Composable +fun LoginRoute( + onBackClick: () -> Unit, + modifier: Modifier = Modifier, + viewModel: LoginViewModel = hiltViewModel() +) { + val context = LocalContext.current + + LoginScreen( + modifier = modifier, + onGoogleLoginClick = { viewModel.request(context) } + ) +} + +@Composable +fun LoginScreen( + modifier: Modifier = Modifier, + onGoogleLoginClick: () -> Unit, +) { + Box( + modifier = modifier + ) { + Column( + modifier = modifier + .fillMaxSize() + .background(color = Orange01) + .padding(horizontal = 24.dp), + verticalArrangement = Arrangement.Center + ) { + Box( + modifier = modifier + .size(342.dp) + ) + + Box( + modifier = modifier + .fillMaxWidth() + .padding(top = 91.dp) + .height(60.dp) + .background(color = Color.White, shape = RoundedCornerShape(30.dp)) + .clip(RoundedCornerShape(30.dp)) + .clickable(onClick = onGoogleLoginClick), + contentAlignment = Alignment.CenterStart + ) { + Image( + painter = painterResource(id = R.drawable.btn_google), + contentScale = ContentScale.FillBounds, + contentDescription = null + ) + Text( + text = stringResource(id = R.string.google_login), + modifier = modifier.fillMaxWidth(), + style = MissionMateTypography.body_lg_bold, + color = Color.Black, + textAlign = TextAlign.Center + ) + } + } + } +} + +@Preview +@Composable +fun LoginScreenPreview() { + LoginScreen( + onGoogleLoginClick = {} + ) +} diff --git a/feature/login/src/main/java/com/goalpanzi/mission_mate/feature/login/LoginUiState.kt b/feature/login/src/main/java/com/goalpanzi/mission_mate/feature/login/LoginUiState.kt new file mode 100644 index 00000000..dac6ead8 --- /dev/null +++ b/feature/login/src/main/java/com/goalpanzi/mission_mate/feature/login/LoginUiState.kt @@ -0,0 +1,6 @@ +package com.goalpanzi.mission_mate.feature.login + +sealed interface LoginUiState { + data object Loading : LoginUiState + data class Success(val isAlreadyMember: Boolean) : LoginUiState +} \ No newline at end of file diff --git a/feature/login/src/main/java/com/goalpanzi/mission_mate/feature/login/LoginViewModel.kt b/feature/login/src/main/java/com/goalpanzi/mission_mate/feature/login/LoginViewModel.kt new file mode 100644 index 00000000..83074eea --- /dev/null +++ b/feature/login/src/main/java/com/goalpanzi/mission_mate/feature/login/LoginViewModel.kt @@ -0,0 +1,69 @@ +package com.goalpanzi.mission_mate.feature.login + +import android.content.Context +import androidx.credentials.CredentialManager +import androidx.credentials.CustomCredential +import androidx.credentials.GetCredentialRequest +import androidx.credentials.GetCredentialResponse +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.goalpanzi.mission_mate.core.domain.usecase.LoginUseCase +import com.goalpanzi.mission_mate.feature.login.util.TokenUtil +import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption +import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential +import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class LoginViewModel @Inject constructor( + private val loginUseCase: LoginUseCase +) : ViewModel() { + + fun request(context: Context) { + viewModelScope.launch { + + val credentialManager = CredentialManager.create(context) + val signInWithGoogleOption: GetSignInWithGoogleOption = + GetSignInWithGoogleOption.Builder(BuildConfig.CREDENTIAL_WEB_CLIENT_ID) + .build() + + val request: GetCredentialRequest = GetCredentialRequest.Builder() + .addCredentialOption(signInWithGoogleOption) + .build() + + try { + val result = credentialManager.getCredential(context, request) + handleSignIn(result) + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + private suspend fun handleSignIn(response: GetCredentialResponse) { + when (val credential = response.credential) { + is CustomCredential -> { + if (credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) { + try { + val googleIdTokenCredential = + GoogleIdTokenCredential.createFrom(credential.data) + val compressedToken = TokenUtil.compressToken(googleIdTokenCredential.idToken) + val result = loginUseCase.requestGoogleLogin( + token = compressedToken, + email = googleIdTokenCredential.id + ) + // TODO : success event + } catch (e: GoogleIdTokenParsingException) { + e.printStackTrace() + } + } + } + + else -> { + // TODO : error event + } + } + } +} \ No newline at end of file diff --git a/feature/login/src/main/java/com/goalpanzi/mission_mate/feature/login/util/TokenUtil.kt b/feature/login/src/main/java/com/goalpanzi/mission_mate/feature/login/util/TokenUtil.kt new file mode 100644 index 00000000..6b87dc6f --- /dev/null +++ b/feature/login/src/main/java/com/goalpanzi/mission_mate/feature/login/util/TokenUtil.kt @@ -0,0 +1,10 @@ +package com.goalpanzi.mission_mate.feature.login.util + +import java.security.MessageDigest + +object TokenUtil { + fun compressToken(data: String): String { + val bytes = MessageDigest.getInstance("SHA-256").digest(data.toByteArray()) + return bytes.joinToString("") { "%02x".format(it) } + } +} \ No newline at end of file diff --git a/feature/login/src/main/res/drawable/btn_google.xml b/feature/login/src/main/res/drawable/btn_google.xml new file mode 100644 index 00000000..e099295a --- /dev/null +++ b/feature/login/src/main/res/drawable/btn_google.xml @@ -0,0 +1,29 @@ + + + + + + + + + + diff --git a/feature/login/src/main/res/values/strings.xml b/feature/login/src/main/res/values/strings.xml new file mode 100644 index 00000000..342e82a8 --- /dev/null +++ b/feature/login/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Google로 로그인 + \ No newline at end of file diff --git a/feature/main/build.gradle.kts b/feature/main/build.gradle.kts index 37589087..185bd8e8 100644 --- a/feature/main/build.gradle.kts +++ b/feature/main/build.gradle.kts @@ -69,4 +69,7 @@ dependencies { ksp(libs.hilt.compiler) implementation(project(":core:designsystem")) + implementation(project(":core:navigation")) + implementation(project(":core:domain")) + implementation(project(":feature:login")) } \ No newline at end of file diff --git a/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/MainActivity.kt b/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/MainActivity.kt index db1bbfbb..57f70455 100644 --- a/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/MainActivity.kt +++ b/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/MainActivity.kt @@ -4,45 +4,27 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview +import androidx.activity.viewModels +import com.goalpanzi.mission_mate.core.designsystem.theme.MissionmateTheme +import com.goalpanzi.mission_mate.core.main.component.MainNavigator +import com.goalpanzi.mission_mate.core.main.component.rememberMainNavigator import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class MainActivity : ComponentActivity() { + + private val viewModel: MainViewModel by viewModels() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { - com.goalpanzi.mission_mate.core.designsystem.theme.MissionmateTheme { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> - Greeting( - name = "Android", - modifier = Modifier.padding(innerPadding) - ) - } + val navigator: MainNavigator = rememberMainNavigator() + MissionmateTheme { + MainScreen( + navigator = navigator, + ) } } } } - -@Composable -fun Greeting(name: String, modifier: Modifier = Modifier) { - Text( - text = "Hello $name!", - modifier = modifier - ) -} - -@Preview(showBackground = true) -@Composable -fun GreetingPreview() { - com.goalpanzi.mission_mate.core.designsystem.theme.MissionmateTheme { - Greeting("Android") - } -} \ No newline at end of file diff --git a/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/MainScreen.kt b/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/MainScreen.kt new file mode 100644 index 00000000..3a5366ab --- /dev/null +++ b/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/MainScreen.kt @@ -0,0 +1,33 @@ +package com.goalpanzi.mission_mate.core.main + +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.goalpanzi.mission_mate.core.main.component.MainNavHost +import com.goalpanzi.mission_mate.core.main.component.MainNavigator +import com.goalpanzi.mission_mate.core.main.component.rememberMainNavigator + +@Composable +internal fun MainScreen( + navigator: MainNavigator = rememberMainNavigator(), +) { + MainScreenContent( + navigator = navigator + ) +} + +@Composable +private fun MainScreenContent( + navigator: MainNavigator, + modifier: Modifier = Modifier +) { + Scaffold( + modifier = modifier, + content = { padding -> + MainNavHost( + navigator = navigator, + padding = padding + ) + } + ) +} \ No newline at end of file diff --git a/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/MainViewModel.kt b/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/MainViewModel.kt new file mode 100644 index 00000000..dd508655 --- /dev/null +++ b/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/MainViewModel.kt @@ -0,0 +1,14 @@ +package com.goalpanzi.mission_mate.core.main + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class MainViewModel @Inject constructor( + +): ViewModel() { + + // TODO : flow to get isAuthorized + val isAuthorized = false +} \ No newline at end of file diff --git a/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavHost.kt b/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavHost.kt new file mode 100644 index 00000000..d76c4a9c --- /dev/null +++ b/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavHost.kt @@ -0,0 +1,33 @@ +package com.goalpanzi.mission_mate.core.main.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.compose.NavHost +import com.goalpanzi.mission_mate.feature.login.loginNavGraph + +@Composable +internal fun MainNavHost( + modifier: Modifier = Modifier, + navigator: MainNavigator, + padding: PaddingValues +) { + Box( + modifier = modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surfaceDim) + ) { + NavHost( + navController = navigator.navController, + startDestination = navigator.startDestination + ) { + loginNavGraph( + onBackClick = { navigator.popBackStack() } + ) + } + } +} \ No newline at end of file diff --git a/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavigator.kt b/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavigator.kt new file mode 100644 index 00000000..20f8eaaf --- /dev/null +++ b/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavigator.kt @@ -0,0 +1,31 @@ +package com.goalpanzi.mission_mate.core.main.component + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import com.goalpanzi.mission_mate.core.navigation.RouteModel +import com.goalpanzi.mission_mate.feature.login.navigateToLogin + +class MainNavigator( + val navController: NavHostController +) { + + //TODO : change to Main + val startDestination = RouteModel.Login + + fun popBackStack() { + navController.popBackStack() + } + + fun navigateToLogin() { + navController.navigateToLogin() + } +} + +@Composable +internal fun rememberMainNavigator( + navController: NavHostController = rememberNavController() +) : MainNavigator = remember(navController) { + MainNavigator(navController) +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2b2125b5..4246f804 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ androidxActivity = "1.9.0" ## Compose composeBom = "2024.06.00" -navigation-compose = "2.7.7" +navigation-compose = "2.8.0-beta02" ## Kotlin Symbol Processing (KSP) ksp = "2.0.0-1.0.23" @@ -43,6 +43,11 @@ dataStore-preferences = "1.1.1" ## Image Loader coil-compose = "2.5.0" +## Google OAuth +credential = "1.2.2" +identity = "1.1.1" +material = "1.12.0" + [libraries] ## Koitln kotlin-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } @@ -69,7 +74,7 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" } -androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation-compose" } +androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation-compose" } ## Test junit = { group = "junit", name = "junit", version.ref = "junit" } @@ -97,6 +102,12 @@ dataStore = { module = "androidx.datastore:datastore-preferences", version.ref = ## Image Loader coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil-compose" } +## Google OAuth +credentials = { group = "androidx.credentials", name = "credentials", version.ref = "credential" } +credentials-auth = { group = "androidx.credentials", name = "credentials-play-services-auth", version.ref = "credential" } +google-id = { group = "com.google.android.libraries.identity.googleid", name = "googleid", version = "identity" } +material = { group = "com.google.android.material", name = "material", version.ref = "material" } + [plugins] ## Android gradle plugin android-application = { id = "com.android.application", version.ref = "agp" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1789af6b..60df1575 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,4 +1,4 @@ -#Sat Jul 13 15:21:07 KST 2024 +#Sat Jul 20 15:16:45 KST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip diff --git a/settings.gradle.kts b/settings.gradle.kts index 1808e810..c5bf1a21 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -30,3 +30,4 @@ include(":core:navigation") include(":feature:login") include(":feature:main") include(":feature:board") +include(":core:model")