diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c480de3d5..10192b420 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,20 +1,39 @@ +import java.util.Properties + plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.hilt) + alias(libs.plugins.ksp) + alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.ktlint) } +val properties = + Properties().apply { + load(project.rootProject.file("local.properties").inputStream()) + } android { - namespace = "com.android.heartz" - compileSdk = 35 + namespace = "com.heartz.app" + compileSdk = libs.versions.compileSdk.get().toInt() defaultConfig { - applicationId = "com.android.heartz" - minSdk = 28 - targetSdk = 35 - versionCode = 1 - versionName = "1.0" + applicationId = "com.heartz.app" + minSdk = libs.versions.minSdk.get().toInt() + targetSdk = libs.versions.targetSdk.get().toInt() + versionCode = libs.versions.versionCode.get().toInt() + versionName = libs.versions.versionName.get() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + buildConfigField("String", "BASE_URL", properties["base.url"].toString()) + buildConfigField( + "String", + "KAKAO_NATIVE_APP_KEY", + properties["kakao.native.app.key"].toString() + ) } buildTypes { @@ -27,20 +46,66 @@ android { } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = "11" + jvmTarget = "17" + } + buildFeatures { + compose = true + buildConfig = true } } dependencies { - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.appcompat) - implementation(libs.material) + // Test testImplementation(libs.junit) - androidTestImplementation(libs.androidx.junit) - androidTestImplementation(libs.androidx.espresso.core) -} \ No newline at end of file + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.bundles.test) + + // Debug + debugImplementation(libs.bundles.debug) + + // AndroidX + implementation(libs.bundles.androidx) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.kotlinx.collections.immutable) + + // Google + implementation(platform(libs.google.firebase.bom)) + implementation(libs.google.firebase.crashlytics) + + // Network + implementation(platform(libs.okhttp.bom)) + implementation(libs.bundles.okhttp) + implementation(libs.bundles.retrofit) + implementation(libs.kotlinx.serialization.json) + + // Hilt + implementation(libs.bundles.hilt) + ksp(libs.hilt.compiler) + + // Coil + implementation(libs.coil.compose) + + // Timber + implementation(libs.timber) + + // Kakao Login + implementation(libs.bundles.kakao) + + // Ui + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + debugImplementation(libs.androidx.ui.tooling) +} + +ktlint { + android = true + debug = true + coloredOutput = true + verbose = true + outputToConsole = true +} diff --git a/app/src/androidTest/java/com/android/heartz/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/android/heartz/ExampleInstrumentedTest.kt index f6e485f64..56eb3020b 100644 --- a/app/src/androidTest/java/com/android/heartz/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/android/heartz/ExampleInstrumentedTest.kt @@ -1,24 +1,16 @@ package com.android.heartz -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 - +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals 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.android.heartz", appContext.packageName) } -} \ No newline at end of file +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1a49ba31c..e8eaf110e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,7 +2,10 @@ + + + tools:targetApi="31"> + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/heartz/app/Heartz.kt b/app/src/main/java/com/heartz/app/Heartz.kt new file mode 100644 index 000000000..4817d0e04 --- /dev/null +++ b/app/src/main/java/com/heartz/app/Heartz.kt @@ -0,0 +1,24 @@ +package com.heartz.app + +import android.app.Application +import androidx.appcompat.app.AppCompatDelegate +import dagger.hilt.android.HiltAndroidApp +import timber.log.Timber + +@HiltAndroidApp +class Heartz : Application() { + override fun onCreate() { + super.onCreate() + + initTimber() + setDayMode() + } + + private fun initTimber() { + if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) + } + + private fun setDayMode() { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) + } +} diff --git a/app/src/main/java/com/heartz/app/core/designsystem/ui/theme/Color.kt b/app/src/main/java/com/heartz/app/core/designsystem/ui/theme/Color.kt new file mode 100644 index 000000000..97bca443a --- /dev/null +++ b/app/src/main/java/com/heartz/app/core/designsystem/ui/theme/Color.kt @@ -0,0 +1,13 @@ +package com.heartz.app.core.designsystem.ui.theme + +import androidx.compose.material3.darkColorScheme +import androidx.compose.ui.graphics.Color + +val Red80 = Color(0xFFFF5656) +val Pink80 = Color(0xFFFFB0B0) + +val HeartzColorScheme = + darkColorScheme( + primary = Red80, + secondary = Pink80 + ) diff --git a/app/src/main/java/com/heartz/app/core/designsystem/ui/theme/Theme.kt b/app/src/main/java/com/heartz/app/core/designsystem/ui/theme/Theme.kt new file mode 100644 index 000000000..608d4b9df --- /dev/null +++ b/app/src/main/java/com/heartz/app/core/designsystem/ui/theme/Theme.kt @@ -0,0 +1,13 @@ +package com.heartz.app.core.designsystem.ui.theme + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable + +@Composable +fun HeartzTheme(content: @Composable () -> Unit) { + MaterialTheme( + colorScheme = HeartzColorScheme, + typography = Typography, + content = content + ) +} diff --git a/app/src/main/java/com/heartz/app/core/designsystem/ui/theme/TypoGraphy.kt b/app/src/main/java/com/heartz/app/core/designsystem/ui/theme/TypoGraphy.kt new file mode 100644 index 000000000..8d06639da --- /dev/null +++ b/app/src/main/java/com/heartz/app/core/designsystem/ui/theme/TypoGraphy.kt @@ -0,0 +1,18 @@ +package com.heartz.app.core.designsystem.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +val Typography = + Typography( + bodyLarge = + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.SemiBold, + fontSize = 16.sp, + lineHeight = 24.sp + ) + ) diff --git a/app/src/main/java/com/heartz/app/core/navigation/MainTabRoute.kt b/app/src/main/java/com/heartz/app/core/navigation/MainTabRoute.kt new file mode 100644 index 000000000..03140d061 --- /dev/null +++ b/app/src/main/java/com/heartz/app/core/navigation/MainTabRoute.kt @@ -0,0 +1,3 @@ +package com.heartz.app.core.navigation + +interface MainTabRoute : Route diff --git a/app/src/main/java/com/heartz/app/core/navigation/Route.kt b/app/src/main/java/com/heartz/app/core/navigation/Route.kt new file mode 100644 index 000000000..7a9eeeccd --- /dev/null +++ b/app/src/main/java/com/heartz/app/core/navigation/Route.kt @@ -0,0 +1,3 @@ +package com.heartz.app.core.navigation + +interface Route diff --git a/app/src/main/java/com/heartz/app/core/state/UiState.kt b/app/src/main/java/com/heartz/app/core/state/UiState.kt new file mode 100644 index 000000000..e590e1a85 --- /dev/null +++ b/app/src/main/java/com/heartz/app/core/state/UiState.kt @@ -0,0 +1,15 @@ +package com.heartz.app.core.state + +sealed interface UiState { + data object Empty : UiState + + data object Loading : UiState + + data class Success( + val data: T + ) : UiState + + data class Failure( + val msg: String + ) : UiState +} diff --git a/app/src/main/java/com/heartz/app/core/util/ModifierExt.kt b/app/src/main/java/com/heartz/app/core/util/ModifierExt.kt new file mode 100644 index 000000000..68ac49fa5 --- /dev/null +++ b/app/src/main/java/com/heartz/app/core/util/ModifierExt.kt @@ -0,0 +1,17 @@ +package com.heartz.app.core.util + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed + +inline fun Modifier.noRippleClickable(crossinline onClick: () -> Unit = {}): Modifier = + composed { + this.clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + onClick() + } + } diff --git a/app/src/main/java/com/heartz/app/data/datasource/local/DummyLocalDataSource.kt b/app/src/main/java/com/heartz/app/data/datasource/local/DummyLocalDataSource.kt new file mode 100644 index 000000000..070aee8b9 --- /dev/null +++ b/app/src/main/java/com/heartz/app/data/datasource/local/DummyLocalDataSource.kt @@ -0,0 +1,12 @@ +package com.heartz.app.data.datasource.local + +import kotlinx.coroutines.flow.Flow + +// TODO: 임시 +interface DummyLocalDataSource { + val isLogin: Flow + + suspend fun setIsLogin(value: Boolean) + + suspend fun clear() +} diff --git a/app/src/main/java/com/heartz/app/data/datasource/remote/DummyRemoteDataSource.kt b/app/src/main/java/com/heartz/app/data/datasource/remote/DummyRemoteDataSource.kt new file mode 100644 index 000000000..d7f2b509b --- /dev/null +++ b/app/src/main/java/com/heartz/app/data/datasource/remote/DummyRemoteDataSource.kt @@ -0,0 +1,9 @@ +package com.heartz.app.data.datasource.remote + +import com.heartz.app.data.dto.base.DummyBaseResponse +import com.heartz.app.data.dto.request.RequestDummyDto +import com.heartz.app.data.dto.response.ResponseDummyDto + +interface DummyRemoteDataSource { + suspend fun getDummies(request: RequestDummyDto): DummyBaseResponse +} diff --git a/app/src/main/java/com/heartz/app/data/datasourceimpl/local/DummyLocalDataSourceImpl.kt b/app/src/main/java/com/heartz/app/data/datasourceimpl/local/DummyLocalDataSourceImpl.kt new file mode 100644 index 000000000..d4af76a55 --- /dev/null +++ b/app/src/main/java/com/heartz/app/data/datasourceimpl/local/DummyLocalDataSourceImpl.kt @@ -0,0 +1,36 @@ +package com.heartz.app.data.datasourceimpl.local + +import android.content.Context +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.preferencesDataStore +import com.heartz.app.data.datasource.local.DummyLocalDataSource +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +private const val FILE_NAME = "heartz_datastore" + +private val Context.dataStore by preferencesDataStore(name = FILE_NAME) + +class DummyLocalDataSourceImpl @Inject constructor( + @ApplicationContext private val context: Context +) : DummyLocalDataSource { + companion object { + val IS_LOGIN = booleanPreferencesKey("is_login") + } + + override val isLogin: Flow = + context.dataStore.data.map { preferences -> + preferences[IS_LOGIN] ?: false + } + + override suspend fun setIsLogin(value: Boolean) { + context.dataStore.edit { it[IS_LOGIN] = value } + } + + override suspend fun clear() { + context.dataStore.edit { it.clear() } + } +} diff --git a/app/src/main/java/com/heartz/app/data/datasourceimpl/remote/DummyRemoteDataSourceImpl.kt b/app/src/main/java/com/heartz/app/data/datasourceimpl/remote/DummyRemoteDataSourceImpl.kt new file mode 100644 index 000000000..f432b52be --- /dev/null +++ b/app/src/main/java/com/heartz/app/data/datasourceimpl/remote/DummyRemoteDataSourceImpl.kt @@ -0,0 +1,19 @@ +package com.heartz.app.data.datasourceimpl.remote + +import com.heartz.app.data.datasource.remote.DummyRemoteDataSource +import com.heartz.app.data.dto.base.DummyBaseResponse +import com.heartz.app.data.dto.request.RequestDummyDto +import com.heartz.app.data.dto.response.ResponseDummyDto +import com.heartz.app.data.service.DummyService +import javax.inject.Inject + +class DummyRemoteDataSourceImpl +@Inject +constructor( + private val dummyService: DummyService +) : DummyRemoteDataSource { + override suspend fun getDummies( + request: RequestDummyDto + ): DummyBaseResponse = + dummyService.getDummies(request) +} diff --git a/app/src/main/java/com/heartz/app/data/di/DataSourceModule.kt b/app/src/main/java/com/heartz/app/data/di/DataSourceModule.kt new file mode 100644 index 000000000..8512122fc --- /dev/null +++ b/app/src/main/java/com/heartz/app/data/di/DataSourceModule.kt @@ -0,0 +1,23 @@ +package com.heartz.app.data.di + +import com.heartz.app.data.datasource.local.DummyLocalDataSource +import com.heartz.app.data.datasource.remote.DummyRemoteDataSource +import com.heartz.app.data.datasourceimpl.local.DummyLocalDataSourceImpl +import com.heartz.app.data.datasourceimpl.remote.DummyRemoteDataSourceImpl +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class DataSourceModule { + @Binds + @Singleton + abstract fun bindDummyRemoteDataSource(impl: DummyRemoteDataSourceImpl): DummyRemoteDataSource + + @Binds + @Singleton + abstract fun bindDummyLocalDataSource(impl: DummyLocalDataSourceImpl): DummyLocalDataSource +} diff --git a/app/src/main/java/com/heartz/app/data/di/NetworkModule.kt b/app/src/main/java/com/heartz/app/data/di/NetworkModule.kt new file mode 100644 index 000000000..21a17dba3 --- /dev/null +++ b/app/src/main/java/com/heartz/app/data/di/NetworkModule.kt @@ -0,0 +1,53 @@ +package com.heartz.app.data.di + +import com.heartz.app.BuildConfig +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Converter +import retrofit2.Retrofit + +@Module +@InstallIn(SingletonComponent::class) +object NetworkModule { + @Provides + @Singleton + fun providesLoggingInterceptor() = + HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + } + + @Provides + @Singleton + fun providesOkHttpClient(loggingInterceptor: HttpLoggingInterceptor): OkHttpClient = + OkHttpClient.Builder() + .addInterceptor(loggingInterceptor) + .build() + + @OptIn(ExperimentalSerializationApi::class) + @Provides + @Singleton + fun providesConverterFactory(): Converter.Factory = Json.asConverterFactory( + "application/json".toMediaType() + ) + + @Provides + @Singleton + fun providesRetrofit( + client: OkHttpClient, + converterFactory: Converter.Factory + ): Retrofit = + Retrofit.Builder() + .baseUrl(BuildConfig.BASE_URL) + .client(client) + .addConverterFactory(converterFactory) + .build() +} diff --git a/app/src/main/java/com/heartz/app/data/di/RepositoryModule.kt b/app/src/main/java/com/heartz/app/data/di/RepositoryModule.kt new file mode 100644 index 000000000..b04fd17f7 --- /dev/null +++ b/app/src/main/java/com/heartz/app/data/di/RepositoryModule.kt @@ -0,0 +1,17 @@ +package com.heartz.app.data.di + +import com.heartz.app.data.repositoryimpl.DummyRepositoryImpl +import com.heartz.app.domain.repository.DummyRepository +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class RepositoryModule { + @Binds + @Singleton + abstract fun bindsDummyRepository(dummyRepositoryImpl: DummyRepositoryImpl): DummyRepository +} diff --git a/app/src/main/java/com/heartz/app/data/di/ServiceModule.kt b/app/src/main/java/com/heartz/app/data/di/ServiceModule.kt new file mode 100644 index 000000000..261e8a561 --- /dev/null +++ b/app/src/main/java/com/heartz/app/data/di/ServiceModule.kt @@ -0,0 +1,19 @@ +package com.heartz.app.data.di + +import com.heartz.app.data.service.DummyService +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton +import retrofit2.Retrofit + +@Module +@InstallIn(SingletonComponent::class) +object ServiceModule { + @Provides + @Singleton + fun providesDummyService(retrofit: Retrofit): DummyService = retrofit.create( + DummyService::class.java + ) +} diff --git a/app/src/main/java/com/heartz/app/data/dto/base/DummyBaseResponse.kt b/app/src/main/java/com/heartz/app/data/dto/base/DummyBaseResponse.kt new file mode 100644 index 000000000..efbb0fc31 --- /dev/null +++ b/app/src/main/java/com/heartz/app/data/dto/base/DummyBaseResponse.kt @@ -0,0 +1,16 @@ +package com.heartz.app.data.dto.base + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class DummyBaseResponse( + @SerialName("success") + val success: Boolean, + @SerialName("code") + val code: String, + @SerialName("message") + val message: String, + @SerialName("data") + val data: T +) diff --git a/app/src/main/java/com/heartz/app/data/dto/base/DummyNullableBaseResponse.kt b/app/src/main/java/com/heartz/app/data/dto/base/DummyNullableBaseResponse.kt new file mode 100644 index 000000000..d67cc6e50 --- /dev/null +++ b/app/src/main/java/com/heartz/app/data/dto/base/DummyNullableBaseResponse.kt @@ -0,0 +1,16 @@ +package com.heartz.app.data.dto.base + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class DummyNullableBaseResponse( + @SerialName("success") + val success: Boolean, + @SerialName("code") + val code: String, + @SerialName("message") + val message: String, + @SerialName("data") + val data: T? = null +) diff --git a/app/src/main/java/com/heartz/app/data/dto/request/RequestDummyDto.kt b/app/src/main/java/com/heartz/app/data/dto/request/RequestDummyDto.kt new file mode 100644 index 000000000..fb15987f0 --- /dev/null +++ b/app/src/main/java/com/heartz/app/data/dto/request/RequestDummyDto.kt @@ -0,0 +1,12 @@ +package com.heartz.app.data.dto.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RequestDummyDto( + @SerialName("id") + val id: Int, + @SerialName("email") + val email: String +) diff --git a/app/src/main/java/com/heartz/app/data/dto/response/ResponseDummyDto.kt b/app/src/main/java/com/heartz/app/data/dto/response/ResponseDummyDto.kt new file mode 100644 index 000000000..b3b9adb60 --- /dev/null +++ b/app/src/main/java/com/heartz/app/data/dto/response/ResponseDummyDto.kt @@ -0,0 +1,10 @@ +package com.heartz.app.data.dto.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseDummyDto( + @SerialName("info") + val info: List +) diff --git a/app/src/main/java/com/heartz/app/data/mapper/todata/DummyMapper.kt b/app/src/main/java/com/heartz/app/data/mapper/todata/DummyMapper.kt new file mode 100644 index 000000000..05c1f3088 --- /dev/null +++ b/app/src/main/java/com/heartz/app/data/mapper/todata/DummyMapper.kt @@ -0,0 +1,10 @@ +package com.heartz.app.data.mapper.todata + +import com.heartz.app.data.dto.request.RequestDummyDto +import com.heartz.app.domain.model.Dummy + +fun Dummy.toData(): RequestDummyDto = + RequestDummyDto( + id = this.id, + email = this.email + ) diff --git a/app/src/main/java/com/heartz/app/data/mapper/todomain/ResponseDummyDtoMapper.kt b/app/src/main/java/com/heartz/app/data/mapper/todomain/ResponseDummyDtoMapper.kt new file mode 100644 index 000000000..0d39273a0 --- /dev/null +++ b/app/src/main/java/com/heartz/app/data/mapper/todomain/ResponseDummyDtoMapper.kt @@ -0,0 +1,9 @@ +package com.heartz.app.data.mapper.todomain + +import com.heartz.app.data.dto.response.ResponseDummyDto +import com.heartz.app.domain.model.DummyResultModel + +fun ResponseDummyDto.toDomain(): DummyResultModel = + DummyResultModel( + info = info + ) diff --git a/app/src/main/java/com/heartz/app/data/repositoryimpl/DummyRepositoryImpl.kt b/app/src/main/java/com/heartz/app/data/repositoryimpl/DummyRepositoryImpl.kt new file mode 100644 index 000000000..c009e1412 --- /dev/null +++ b/app/src/main/java/com/heartz/app/data/repositoryimpl/DummyRepositoryImpl.kt @@ -0,0 +1,21 @@ +package com.heartz.app.data.repositoryimpl + +import com.heartz.app.data.mapper.todata.toData +import com.heartz.app.data.mapper.todomain.toDomain +import com.heartz.app.data.service.DummyService +import com.heartz.app.domain.model.Dummy +import com.heartz.app.domain.model.DummyResultModel +import com.heartz.app.domain.repository.DummyRepository +import javax.inject.Inject + +class DummyRepositoryImpl +@Inject +constructor( + private val dummyService: DummyService +) : DummyRepository { + override suspend fun getDummies(request: Dummy): Result = + runCatching { + val response = dummyService.getDummies(request = request.toData()) + response.data.toDomain() + } +} diff --git a/app/src/main/java/com/heartz/app/data/service/DummyService.kt b/app/src/main/java/com/heartz/app/data/service/DummyService.kt new file mode 100644 index 000000000..fbb5b1fa8 --- /dev/null +++ b/app/src/main/java/com/heartz/app/data/service/DummyService.kt @@ -0,0 +1,15 @@ +package com.heartz.app.data.service + +import com.heartz.app.data.dto.base.DummyBaseResponse +import com.heartz.app.data.dto.request.RequestDummyDto +import com.heartz.app.data.dto.response.ResponseDummyDto +import retrofit2.http.Body +import retrofit2.http.POST + +interface DummyService { + // TODO: 이름은 getDummies지만 @Body를 사용해버려서 post로 바꿨습니다,, 임시! + @POST("/api/v1/service") + suspend fun getDummies( + @Body request: RequestDummyDto + ): DummyBaseResponse +} diff --git a/app/src/main/java/com/heartz/app/domain/model/Dummy.kt b/app/src/main/java/com/heartz/app/domain/model/Dummy.kt new file mode 100644 index 000000000..22cd1d996 --- /dev/null +++ b/app/src/main/java/com/heartz/app/domain/model/Dummy.kt @@ -0,0 +1,6 @@ +package com.heartz.app.domain.model + +data class Dummy( + val id: Int, + val email: String +) diff --git a/app/src/main/java/com/heartz/app/domain/model/DummyResultModel.kt b/app/src/main/java/com/heartz/app/domain/model/DummyResultModel.kt new file mode 100644 index 000000000..058207640 --- /dev/null +++ b/app/src/main/java/com/heartz/app/domain/model/DummyResultModel.kt @@ -0,0 +1,5 @@ +package com.heartz.app.domain.model + +data class DummyResultModel( + val info: List +) diff --git a/app/src/main/java/com/heartz/app/domain/repository/DummyRepository.kt b/app/src/main/java/com/heartz/app/domain/repository/DummyRepository.kt new file mode 100644 index 000000000..9c9768244 --- /dev/null +++ b/app/src/main/java/com/heartz/app/domain/repository/DummyRepository.kt @@ -0,0 +1,8 @@ +package com.heartz.app.domain.repository + +import com.heartz.app.domain.model.Dummy +import com.heartz.app.domain.model.DummyResultModel + +interface DummyRepository { + suspend fun getDummies(request: Dummy): Result +} diff --git a/app/src/main/java/com/heartz/app/domain/usecase/DummyUseCase.kt b/app/src/main/java/com/heartz/app/domain/usecase/DummyUseCase.kt new file mode 100644 index 000000000..03c763695 --- /dev/null +++ b/app/src/main/java/com/heartz/app/domain/usecase/DummyUseCase.kt @@ -0,0 +1,5 @@ +package com.heartz.app.domain.usecase + +// / TODO: usecase는 생각이 많은데 고민을 조금 해봐야 겠습니다! + +interface DummyUseCase diff --git a/app/src/main/java/com/heartz/app/presentation/graph/FigureScreen.kt b/app/src/main/java/com/heartz/app/presentation/graph/FigureScreen.kt new file mode 100644 index 000000000..0832af328 --- /dev/null +++ b/app/src/main/java/com/heartz/app/presentation/graph/FigureScreen.kt @@ -0,0 +1,27 @@ +package com.heartz.app.presentation.graph + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color + +@Composable +fun FigureScreen() { + Column( + modifier = Modifier + .fillMaxSize() + .background(Color.White), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "figure Screen", + color = Color.Black + ) + } +} diff --git a/app/src/main/java/com/heartz/app/presentation/graph/navigation/FigureNavigation.kt b/app/src/main/java/com/heartz/app/presentation/graph/navigation/FigureNavigation.kt new file mode 100644 index 000000000..2ea2c5239 --- /dev/null +++ b/app/src/main/java/com/heartz/app/presentation/graph/navigation/FigureNavigation.kt @@ -0,0 +1,22 @@ +package com.heartz.app.presentation.graph.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.heartz.app.core.navigation.MainTabRoute +import com.heartz.app.presentation.graph.FigureScreen +import kotlinx.serialization.Serializable + +fun NavController.navigateToFigure(navOptions: NavOptions? = null) { + navigate(Figure, navOptions) +} + +fun NavGraphBuilder.figureGraph() { + composable
{ + FigureScreen() + } +} + +@Serializable +data object Figure : MainTabRoute diff --git a/app/src/main/java/com/heartz/app/presentation/home/HomeScreen.kt b/app/src/main/java/com/heartz/app/presentation/home/HomeScreen.kt new file mode 100644 index 000000000..bc6d5d4ad --- /dev/null +++ b/app/src/main/java/com/heartz/app/presentation/home/HomeScreen.kt @@ -0,0 +1,48 @@ +package com.heartz.app.presentation.home + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.heartz.app.core.state.UiState + +@Composable +fun HomeScreen(viewModel: HomeViewModel = hiltViewModel()) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + Column( + modifier = Modifier + .fillMaxSize() + .background(Color.White), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + when (val state = uiState.user) { + is UiState.Loading -> { + Text(text = "로딩 중...", color = Color.Gray) + } + + is UiState.Success -> { + state.data.info.forEachIndexed { index, item -> + Text(text = "Info[$index]: $item") + } + } + + is UiState.Failure -> { + Text(text = "에러: ${state.msg}", color = Color.Red) + } + + UiState.Empty -> { + Text(text = "데이터가 없습니다.") + } + } + } +} diff --git a/app/src/main/java/com/heartz/app/presentation/home/HomeState.kt b/app/src/main/java/com/heartz/app/presentation/home/HomeState.kt new file mode 100644 index 000000000..23454c1c8 --- /dev/null +++ b/app/src/main/java/com/heartz/app/presentation/home/HomeState.kt @@ -0,0 +1,8 @@ +package com.heartz.app.presentation.home + +import com.heartz.app.core.state.UiState +import com.heartz.app.domain.model.DummyResultModel + +data class HomeState( + var user: UiState = UiState.Loading +) diff --git a/app/src/main/java/com/heartz/app/presentation/home/HomeViewModel.kt b/app/src/main/java/com/heartz/app/presentation/home/HomeViewModel.kt new file mode 100644 index 000000000..7d93191ef --- /dev/null +++ b/app/src/main/java/com/heartz/app/presentation/home/HomeViewModel.kt @@ -0,0 +1,45 @@ +package com.heartz.app.presentation.home + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.heartz.app.core.state.UiState +import com.heartz.app.domain.model.Dummy +import com.heartz.app.domain.repository.DummyRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +@HiltViewModel +class HomeViewModel +@Inject +constructor( + private val dummyRepository: DummyRepository +) : ViewModel() { + var uiState = MutableStateFlow(HomeState()) + private set + + fun getDummies( + id: Int, + email: String + ) { + viewModelScope.launch { + dummyRepository.getDummies( + request = Dummy(id = id, email = email) + ) + .onSuccess { response -> + uiState.update { + it.copy( + user = UiState.Success(response) + ) + } + } + .onFailure { e -> + uiState.update { + it.copy(user = UiState.Failure(e.message ?: "오류 발생")) + } + } + } + } +} diff --git a/app/src/main/java/com/heartz/app/presentation/home/navigation/HomeNavigation.kt b/app/src/main/java/com/heartz/app/presentation/home/navigation/HomeNavigation.kt new file mode 100644 index 000000000..7333e77b0 --- /dev/null +++ b/app/src/main/java/com/heartz/app/presentation/home/navigation/HomeNavigation.kt @@ -0,0 +1,22 @@ +package com.heartz.app.presentation.home.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.heartz.app.core.navigation.MainTabRoute +import com.heartz.app.presentation.home.HomeScreen +import kotlinx.serialization.Serializable + +fun NavController.navigateToHome(navOptions: NavOptions? = null) { + navigate(Home, navOptions) +} + +fun NavGraphBuilder.homeGraph() { + composable { + HomeScreen() + } +} + +@Serializable +data object Home : MainTabRoute diff --git a/app/src/main/java/com/heartz/app/presentation/main/MainActivity.kt b/app/src/main/java/com/heartz/app/presentation/main/MainActivity.kt new file mode 100644 index 000000000..f499a8c9e --- /dev/null +++ b/app/src/main/java/com/heartz/app/presentation/main/MainActivity.kt @@ -0,0 +1,21 @@ +package com.heartz.app.presentation.main + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import com.heartz.app.core.designsystem.ui.theme.HeartzTheme +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + HeartzTheme { + MainScreen() + } + } + } +} diff --git a/app/src/main/java/com/heartz/app/presentation/main/MainNavHost.kt b/app/src/main/java/com/heartz/app/presentation/main/MainNavHost.kt new file mode 100644 index 000000000..33c581997 --- /dev/null +++ b/app/src/main/java/com/heartz/app/presentation/main/MainNavHost.kt @@ -0,0 +1,31 @@ +package com.heartz.app.presentation.main + +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.compose.NavHost +import com.heartz.app.presentation.graph.navigation.figureGraph +import com.heartz.app.presentation.home.navigation.homeGraph +import com.heartz.app.presentation.mypage.navigation.mypageGraph +import com.heartz.app.presentation.quest.navigation.questGraph + +@Composable +fun MainNavHost( + navigator: MainNavigator, + modifier: Modifier = Modifier +) { + NavHost( + enterTransition = { EnterTransition.None }, + exitTransition = { ExitTransition.None }, + popEnterTransition = { EnterTransition.None }, + popExitTransition = { ExitTransition.None }, + navController = navigator.navController, + startDestination = navigator.startDestination + ) { + homeGraph() + questGraph() + figureGraph() + mypageGraph() + } +} diff --git a/app/src/main/java/com/heartz/app/presentation/main/MainNavTab.kt b/app/src/main/java/com/heartz/app/presentation/main/MainNavTab.kt new file mode 100644 index 000000000..bae483079 --- /dev/null +++ b/app/src/main/java/com/heartz/app/presentation/main/MainNavTab.kt @@ -0,0 +1,58 @@ +package com.heartz.app.presentation.main + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.compose.runtime.Composable +import com.heartz.app.R.drawable.ic_folder_open +import com.heartz.app.R.drawable.ic_graph +import com.heartz.app.R.drawable.ic_home +import com.heartz.app.R.drawable.ic_user +import com.heartz.app.R.string.ic_figure_desc +import com.heartz.app.R.string.ic_home_desc +import com.heartz.app.R.string.ic_mypage_desc +import com.heartz.app.R.string.ic_quest_desc +import com.heartz.app.core.navigation.MainTabRoute +import com.heartz.app.core.navigation.Route +import com.heartz.app.presentation.graph.navigation.Figure +import com.heartz.app.presentation.home.navigation.Home +import com.heartz.app.presentation.mypage.navigation.Mypage +import com.heartz.app.presentation.quest.navigation.Quest + +enum class MainNavTab( + @DrawableRes val icon: Int, + @StringRes val contentDescription: Int, + val route: MainTabRoute +) { + QUEST( + icon = ic_folder_open, + contentDescription = ic_quest_desc, + route = Quest + ), + HOME( + icon = ic_home, + contentDescription = ic_home_desc, + route = Home + ), + FIGURE( + icon = ic_graph, + contentDescription = ic_figure_desc, + route = Figure + ), + MYPAGE( + icon = ic_user, + contentDescription = ic_mypage_desc, + route = Mypage + ); + + companion object { + @Composable + fun find(predicate: @Composable (MainTabRoute) -> Boolean): MainNavTab? { + return entries.find { predicate(it.route) } + } + + @Composable + fun contains(predicate: @Composable (Route) -> Boolean): Boolean { + return entries.map { it.route }.any { predicate(it) } + } + } +} diff --git a/app/src/main/java/com/heartz/app/presentation/main/MainNavigator.kt b/app/src/main/java/com/heartz/app/presentation/main/MainNavigator.kt new file mode 100644 index 000000000..5cc90a56d --- /dev/null +++ b/app/src/main/java/com/heartz/app/presentation/main/MainNavigator.kt @@ -0,0 +1,69 @@ +package com.heartz.app.presentation.main + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.navigation.NavDestination +import androidx.navigation.NavDestination.Companion.hasRoute +import androidx.navigation.NavHostController +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navOptions +import com.heartz.app.presentation.graph.navigation.navigateToFigure +import com.heartz.app.presentation.home.navigation.Home +import com.heartz.app.presentation.home.navigation.navigateToHome +import com.heartz.app.presentation.mypage.navigation.navigateToMypage +import com.heartz.app.presentation.quest.navigation.navigateToQuest + +class MainNavigator( + val navController: NavHostController +) { + private val currentDestination: NavDestination? + @Composable get() = + navController + .currentBackStackEntryAsState().value?.destination + + val startDestination = Home + + val currentTab: MainNavTab? + @Composable get() = + MainNavTab.find { tab -> + currentDestination?.hasRoute(tab::class) == true + } + + fun navigate(tab: MainNavTab) { + val navOptions = + navOptions { + navController.currentDestination?.route?.let { + popUpTo(it) { + inclusive = true + saveState = true + } + } + launchSingleTop = true + restoreState = true + } + when (tab) { + MainNavTab.QUEST -> navController.navigateToQuest(navOptions) + MainNavTab.HOME -> navController.navigateToHome(navOptions) + MainNavTab.FIGURE -> navController.navigateToFigure(navOptions) + MainNavTab.MYPAGE -> navController.navigateToMypage(navOptions) + } + } + + @Composable + fun showBottomBar() = + MainNavTab.contains { + currentDestination?.hasRoute(it::class) == true + } + + fun navigateUp() { + navController.navigateUp() + } +} + +@Composable +fun rememberMainNavigator( + navController: NavHostController = rememberNavController() +): MainNavigator = remember(navController) { + MainNavigator(navController) +} diff --git a/app/src/main/java/com/heartz/app/presentation/main/MainScreen.kt b/app/src/main/java/com/heartz/app/presentation/main/MainScreen.kt new file mode 100644 index 000000000..a07bc5cb5 --- /dev/null +++ b/app/src/main/java/com/heartz/app/presentation/main/MainScreen.kt @@ -0,0 +1,36 @@ +package com.heartz.app.presentation.main + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.heartz.app.presentation.main.component.MainBottomBar +import kotlinx.collections.immutable.toImmutableList + +@Composable +fun MainScreen(navigator: MainNavigator = rememberMainNavigator()) { + Scaffold( + bottomBar = { + MainBottomBar( + visible = navigator.showBottomBar(), + tabs = MainNavTab.entries.toImmutableList(), + currentTab = navigator.currentTab, + onTabSelected = navigator::navigate + ) + }, + modifier = + Modifier + .background(Color.White) + .systemBarsPadding() + .fillMaxSize() + ) { innerPadding -> + MainNavHost( + navigator = navigator, + modifier = Modifier.padding(innerPadding) + ) + } +} diff --git a/app/src/main/java/com/heartz/app/presentation/main/component/MainBottomBar.kt b/app/src/main/java/com/heartz/app/presentation/main/component/MainBottomBar.kt new file mode 100644 index 000000000..736d15d45 --- /dev/null +++ b/app/src/main/java/com/heartz/app/presentation/main/component/MainBottomBar.kt @@ -0,0 +1,114 @@ +package com.heartz.app.presentation.main.component + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.heartz.app.core.designsystem.ui.theme.HeartzTheme +import com.heartz.app.core.designsystem.ui.theme.Pink80 +import com.heartz.app.core.designsystem.ui.theme.Red80 +import com.heartz.app.core.util.noRippleClickable +import com.heartz.app.presentation.main.MainNavTab +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +@Composable +fun MainBottomBar( + visible: Boolean, + tabs: ImmutableList, + currentTab: MainNavTab?, + onTabSelected: (MainNavTab) -> Unit +) { + AnimatedVisibility( + visible = visible, + enter = EnterTransition.None, + exit = ExitTransition.None + ) { + Column( + modifier = + Modifier + .background(Color.White) + ) { + Row( + modifier = + Modifier + .navigationBarsPadding() + .fillMaxWidth() + .padding(vertical = 21.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceEvenly + ) { + tabs.forEach { tab -> + key(tab.route) { + val selected = currentTab == tab + MainBottomBarItem( + tab = tab, + selected = selected, + onClick = { onTabSelected(tab) } + ) + } + } + } + } + } +} + +@Composable +fun RowScope.MainBottomBarItem( + modifier: Modifier = Modifier, + tab: MainNavTab, + selected: Boolean, + onClick: () -> Unit +) { + val bottomItemColor = if (selected) Red80 else Pink80 + Column( + modifier = + modifier + .noRippleClickable(onClick = onClick) + .weight(1f), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(1.dp) + ) { + Icon( + imageVector = ImageVector.vectorResource(tab.icon), + contentDescription = stringResource(tab.contentDescription), + tint = bottomItemColor + ) + } +} + +@Preview +@Composable +private fun MainBottomBarPreview() { + HeartzTheme { + var currentTab by remember { mutableStateOf(MainNavTab.HOME) } + MainBottomBar( + visible = true, + tabs = MainNavTab.entries.toImmutableList(), + currentTab = currentTab, + onTabSelected = { currentTab = it } + ) + } +} diff --git a/app/src/main/java/com/heartz/app/presentation/mypage/MypageScreen.kt b/app/src/main/java/com/heartz/app/presentation/mypage/MypageScreen.kt new file mode 100644 index 000000000..77d598fac --- /dev/null +++ b/app/src/main/java/com/heartz/app/presentation/mypage/MypageScreen.kt @@ -0,0 +1,27 @@ +package com.heartz.app.presentation.mypage + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color + +@Composable +fun MypageScreen() { + Column( + modifier = Modifier + .fillMaxSize() + .background(Color.White), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "mypage Screen", + color = Color.Black + ) + } +} diff --git a/app/src/main/java/com/heartz/app/presentation/mypage/navigation/MypageNavigation.kt b/app/src/main/java/com/heartz/app/presentation/mypage/navigation/MypageNavigation.kt new file mode 100644 index 000000000..89f83007d --- /dev/null +++ b/app/src/main/java/com/heartz/app/presentation/mypage/navigation/MypageNavigation.kt @@ -0,0 +1,22 @@ +package com.heartz.app.presentation.mypage.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.heartz.app.core.navigation.MainTabRoute +import com.heartz.app.presentation.mypage.MypageScreen +import kotlinx.serialization.Serializable + +fun NavController.navigateToMypage(navOptions: NavOptions? = null) { + navigate(Mypage, navOptions) +} + +fun NavGraphBuilder.mypageGraph() { + composable { + MypageScreen() + } +} + +@Serializable +data object Mypage : MainTabRoute diff --git a/app/src/main/java/com/heartz/app/presentation/quest/QuestScreen.kt b/app/src/main/java/com/heartz/app/presentation/quest/QuestScreen.kt new file mode 100644 index 000000000..aaa0d0009 --- /dev/null +++ b/app/src/main/java/com/heartz/app/presentation/quest/QuestScreen.kt @@ -0,0 +1,27 @@ +package com.heartz.app.presentation.quest + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color + +@Composable +fun QuestScreen() { + Column( + modifier = Modifier + .fillMaxSize() + .background(Color.White), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "quest Screen", + color = Color.Black + ) + } +} diff --git a/app/src/main/java/com/heartz/app/presentation/quest/navigation/QuestNavigation.kt b/app/src/main/java/com/heartz/app/presentation/quest/navigation/QuestNavigation.kt new file mode 100644 index 000000000..fabfda568 --- /dev/null +++ b/app/src/main/java/com/heartz/app/presentation/quest/navigation/QuestNavigation.kt @@ -0,0 +1,22 @@ +package com.heartz.app.presentation.quest.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.heartz.app.core.navigation.MainTabRoute +import com.heartz.app.presentation.quest.QuestScreen +import kotlinx.serialization.Serializable + +fun NavController.navigateToQuest(navOptions: NavOptions? = null) { + navigate(Quest, navOptions) +} + +fun NavGraphBuilder.questGraph() { + composable { + QuestScreen() + } +} + +@Serializable +data object Quest : MainTabRoute diff --git a/app/src/main/res/drawable/ic_folder_open.xml b/app/src/main/res/drawable/ic_folder_open.xml new file mode 100644 index 000000000..835e8fcc6 --- /dev/null +++ b/app/src/main/res/drawable/ic_folder_open.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_graph.xml b/app/src/main/res/drawable/ic_graph.xml new file mode 100644 index 000000000..d8994e7ae --- /dev/null +++ b/app/src/main/res/drawable/ic_graph.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_home.xml b/app/src/main/res/drawable/ic_home.xml new file mode 100644 index 000000000..c53ccc3b4 --- /dev/null +++ b/app/src/main/res/drawable/ic_home.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_user.xml b/app/src/main/res/drawable/ic_user.xml new file mode 100644 index 000000000..372ba8aa2 --- /dev/null +++ b/app/src/main/res/drawable/ic_user.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1e86fbeb7..114d66641 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,10 @@ heartz + + + 퀘스트 + + 그래프 + 마이페이지 + \ No newline at end of file diff --git a/app/src/test/java/com/android/heartz/ExampleUnitTest.kt b/app/src/test/java/com/android/heartz/ExampleUnitTest.kt index bad101c16..a2a4ebf9b 100644 --- a/app/src/test/java/com/android/heartz/ExampleUnitTest.kt +++ b/app/src/test/java/com/android/heartz/ExampleUnitTest.kt @@ -1,17 +1,11 @@ package com.android.heartz +import org.junit.Assert.assertEquals 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/build.gradle.kts b/build.gradle.kts index 922f55110..a619be0cb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,12 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. + plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.kotlin.android) apply false -} \ No newline at end of file + alias(libs.plugins.kotlin.compose) apply false + alias(libs.plugins.kotlin.serialization) apply false + alias(libs.plugins.hilt) apply false + alias(libs.plugins.ksp) apply false + alias(libs.plugins.kotlin.parcelize) apply false + alias(libs.plugins.ktlint) apply false +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9b032167d..f7366827e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,21 +1,164 @@ [versions] -agp = "8.9.1" + +# Version +compileSdk = "35" +minSdk = "28" +targetSdk = "35" +versionCode = "1" +versionName = "1.0" + +# Kotlin +agp = "8.9.2" kotlin = "2.0.21" +kotlinParcelize = "1.8.20" +kotlinxCollectionsImmutable = "0.3.5" + +# AndroidX coreKtx = "1.16.0" +appcompat = "1.7.1" +material = "1.12.0" +material3 = "1.2.0" +lifecycle = "2.8.1" +activityCompose = "1.9.0" +composeBom = "2025.04.01" +navigationCompose = "2.8.9" + +# Test junit = "4.13.2" junitVersion = "1.2.1" espressoCore = "3.6.1" -appcompat = "1.7.1" -material = "1.12.0" +uiTest = "1.6.7" + +# Firebase +firebaseBom = "32.7.4" +crashlytics = "18.6.2" + +# Network +okhttpBom = "4.12.0" +retrofit = "2.11.0" +serialization = "1.6.3" + +# Hilt +hilt = "2.51.1" +hiltCompiler = "2.51.1" +hiltNavigationCompose = "1.2.0" + +# Coil +coil = "2.6.0" + +# Timber +timber = "5.0.1" + +# Kakao +kakao = "2.21.4" + +#Third Party +ksp = "2.0.21-1.0.25" + + + +ktlint = "11.6.1" + + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlinParcelize" } +ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } +hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } + [libraries] +# AndroidX androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +material = { group = "com.google.android.material", name = "material", version.ref = "material" } +material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" } +androidx-lifecycle-runtime = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" } +androidx-lifecycle-viewmodel = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" } +androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } +androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } +androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } +kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinxCollectionsImmutable" } +androidx-ui = { group = "androidx.compose.ui", name = "ui" } +androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } +androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } + +# Test junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } -androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } -material = { group = "com.google.android.material", name = "material", version.ref = "material" } +androidx-ui-test-junit = { group = "androidx.compose.ui", name = "ui-test-junit4", version.ref = "uiTest" } +androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest", version.ref = "uiTest" } + +# Firebase +google-firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" } +google-firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics", version.ref = "crashlytics" } + +# Network +okhttp-bom = { group = "com.squareup.okhttp3", name = "okhttp-bom", version.ref = "okhttpBom" } +okhttp = { group = "com.squareup.okhttp3", name = "okhttp" } +logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor" } +retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } +retrofit-serialization = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version = "0.8.0" } +kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" } + +# Hilt +hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } +hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hiltCompiler" } +androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigationCompose" } + + +# Coil +coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" } + +# Timber +timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" } + +# Kakao +kakao-v2-user = { group = "com.kakao.sdk", name = "v2-user", version.ref = "kakao" } + +[bundles] +androidx = [ + "androidx-core-ktx", + "androidx-appcompat", + "material", + "material3", + "androidx-lifecycle-runtime", + "androidx-lifecycle-viewmodel", + "androidx-activity-compose", + "androidx-navigation-compose" +] + +test = [ + "androidx-ui-test-junit", + "androidx-ui-test-manifest", + "androidx-junit", + "androidx-espresso-core" +] + +debug = [ + "androidx-ui-test-manifest" +] + +okhttp = [ + "okhttp", + "logging-interceptor" +] + +retrofit = [ + "retrofit", + "retrofit-serialization" +] + +hilt = [ + "hilt-android", + "androidx-hilt-navigation-compose" +] -[plugins] -android-application = { id = "com.android.application", version.ref = "agp" } -kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kakao = [ + "kakao-v2-user" +] diff --git a/settings.gradle.kts b/settings.gradle.kts index 2a5a63ddd..4350d7b8c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,6 +16,7 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + maven { url = uri("https://devrepo.kakao.com/nexus/content/groups/public") } } }