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") }
}
}