diff --git a/.github/workflows/android-pull-request-ci.yml b/.github/workflows/android-pull-request-ci.yml index 83f6fe88..5bb75fef 100644 --- a/.github/workflows/android-pull-request-ci.yml +++ b/.github/workflows/android-pull-request-ci.yml @@ -25,6 +25,24 @@ jobs: - name: set up Android SDK uses: android-actions/setup-android@v2 + - name: Access Near DEV_URL + env: + NEAR_DEV_URL: ${{ secrets.NEAR_DEV_URL }} + run: | + echo "NEAR_DEV_URL=\"NEAR_DEV_URL\"" >> local.properties + + - name: Access Near PROD_URL + env: + NEAR_DEV_URL: ${{ secrets.NEAR_PROD_URL }} + run: | + echo "NEAR_PROD_URL=\"NEAR_PROD_URL\"" >> local.properties + + - name: Access Near TEMP_TOKEN + env: + NEAR_DEV_URL: ${{ secrets.TEMP_TOKEN }} + run: | + echo "TEMP_TOKEN=\"TEMP_TOKEN\"" >> local.properties + - name: Grant execute permission for gradlew run: chmod +x gradlew diff --git a/Near/app/build.gradle.kts b/Near/app/build.gradle.kts index f24511cd..bd42f1a5 100644 --- a/Near/app/build.gradle.kts +++ b/Near/app/build.gradle.kts @@ -1,3 +1,5 @@ +import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties + plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) @@ -28,6 +30,12 @@ android { getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro", ) + buildConfigField("String", "NEAR_URL", getProperty("NEAR_PROD_URL")) + buildConfigField("String", "TEMP_TOKEN", getProperty("TEMP_TOKEN")) // TODO 추후 삭제 필요 + } + debug { + buildConfigField("String", "NEAR_URL", getProperty("NEAR_DEV_URL")) + buildConfigField("String", "TEMP_TOKEN", getProperty("TEMP_TOKEN")) // TODO 추후 삭제 필요 } } compileOptions { @@ -38,6 +46,7 @@ android { jvmTarget = "11" } buildFeatures { + buildConfig = true compose = true } } @@ -63,9 +72,10 @@ dependencies { implementation(libs.hilt.android) kapt(libs.hilt.android.compiler) implementation(libs.hilt.navigation.compose) - // Retrofit + // Retrofit & OkHttp implementation(libs.retrofit) - implementation(libs.retrofit.converter.gson) + implementation(libs.retrofit.kotlin.serialization.converter) + implementation(libs.logging.interceptor) // Glide implementation(libs.glide) kapt(libs.glide.compiler) @@ -79,3 +89,5 @@ dependencies { // Serialization implementation(libs.kotlin.serialization.json) } + +fun getProperty(propertyKey: String): String = gradleLocalProperties(rootDir, providers).getProperty(propertyKey) diff --git a/Near/app/src/main/AndroidManifest.xml b/Near/app/src/main/AndroidManifest.xml index 1a2cc18e..08b99e2f 100644 --- a/Near/app/src/main/AndroidManifest.xml +++ b/Near/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + > = + flow { + emit( + friendService.fetchFriends().map { + it.toModel() + }, + ) + } + } diff --git a/Near/app/src/main/java/com/alarmy/near/data/repository/FriendRepository.kt b/Near/app/src/main/java/com/alarmy/near/data/repository/FriendRepository.kt new file mode 100644 index 00000000..b7e76baa --- /dev/null +++ b/Near/app/src/main/java/com/alarmy/near/data/repository/FriendRepository.kt @@ -0,0 +1,8 @@ +package com.alarmy.near.data.repository + +import com.alarmy.near.model.Friend +import kotlinx.coroutines.flow.Flow + +interface FriendRepository { + fun fetchFriends(): Flow> +} diff --git a/Near/app/src/main/java/com/alarmy/near/model/Friend.kt b/Near/app/src/main/java/com/alarmy/near/model/Friend.kt new file mode 100644 index 00000000..ebd71a9b --- /dev/null +++ b/Near/app/src/main/java/com/alarmy/near/model/Friend.kt @@ -0,0 +1,12 @@ +package com.alarmy.near.model + +data class Friend( + val friendId: String, + val position: Int, + val source: String, + val name: String, + val imageUrl: String? = null, + val fileName: String? = null, + val checkRate: Int, + val lastContactAt: String? = null, +) diff --git a/Near/app/src/main/java/com/alarmy/near/network/auth/TokenInterceptor.kt b/Near/app/src/main/java/com/alarmy/near/network/auth/TokenInterceptor.kt new file mode 100644 index 00000000..e2e99566 --- /dev/null +++ b/Near/app/src/main/java/com/alarmy/near/network/auth/TokenInterceptor.kt @@ -0,0 +1,21 @@ +package com.alarmy.near.network.auth + +import com.alarmy.near.BuildConfig +import okhttp3.Interceptor +import okhttp3.Response +import javax.inject.Inject + +class TokenInterceptor + @Inject + constructor() : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + return chain.proceed( + request = + request + .newBuilder() + .addHeader("Authorization", "Bearer ${BuildConfig.TEMP_TOKEN}") + .build(), + ) + } + } diff --git a/Near/app/src/main/java/com/alarmy/near/network/di/AppModule.kt b/Near/app/src/main/java/com/alarmy/near/network/di/AppModule.kt deleted file mode 100644 index 369900cc..00000000 --- a/Near/app/src/main/java/com/alarmy/near/network/di/AppModule.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.alarmy.near.network.di - -// @Module -// @InstallIn(SingletonComponent::class) -// object AppModule { -// @Provides -// @Singleton -// fun provideGson(): Gson = GsonBuilder().create() -// -// @Provides -// @Singleton -// fun provideRetrofit(gson: Gson): Retrofit = -// Retrofit.Builder() -// .baseUrl("https://api.example.com/") // TODO: 실제 baseUrl로 변경 -// .addConverterFactory(GsonConverterFactory.create(gson)) -// .build() -// -// @Provides -// @Singleton -// fun provideDatabase(app: Context): AppDatabase = -// Room.databaseBuilder(app, AppDatabase::class.java, "app_db").build() -// -// @Provides -// @Singleton -// fun provideGlideRequestManager(app: Context): RequestManager = -// Glide.with(app) -// -// @Provides -// @Singleton -// fun provideExampleRepository(): ExampleRepository = ExampleRepositoryImpl() -// } diff --git a/Near/app/src/main/java/com/alarmy/near/network/di/NetworkModule.kt b/Near/app/src/main/java/com/alarmy/near/network/di/NetworkModule.kt new file mode 100644 index 00000000..de4e1e8d --- /dev/null +++ b/Near/app/src/main/java/com/alarmy/near/network/di/NetworkModule.kt @@ -0,0 +1,62 @@ +package com.alarmy.near.network.di + +import com.alarmy.near.BuildConfig +import com.alarmy.near.network.auth.TokenInterceptor +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.kotlinx.serialization.asConverterFactory +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object NetworkModule { + private val json = + Json { + encodeDefaults = true + ignoreUnknownKeys = true + prettyPrint = true + isLenient = true + } + + private val jsonConverterFactory = json.asConverterFactory("application/json".toMediaType()) + + @Provides + @Singleton + fun provideOkHttpClient( + loggingInterceptor: HttpLoggingInterceptor, + tokenInterceptor: TokenInterceptor, + ): OkHttpClient = + OkHttpClient + .Builder() + .addInterceptor(loggingInterceptor) + .addInterceptor(tokenInterceptor) + .build() + + @Provides + @Singleton + fun provideLoggingInterceptor(): HttpLoggingInterceptor = + HttpLoggingInterceptor().let { + if (BuildConfig.DEBUG) { + it.setLevel(HttpLoggingInterceptor.Level.BODY) + } else { + it.setLevel(HttpLoggingInterceptor.Level.NONE) + } + } + + @Provides + @Singleton + fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit = + Retrofit + .Builder() + .baseUrl(BuildConfig.NEAR_URL) + .client(okHttpClient) + .addConverterFactory(jsonConverterFactory) + .build() +} diff --git a/Near/app/src/main/java/com/alarmy/near/network/di/ServiceModule.kt b/Near/app/src/main/java/com/alarmy/near/network/di/ServiceModule.kt new file mode 100644 index 00000000..a6ef4802 --- /dev/null +++ b/Near/app/src/main/java/com/alarmy/near/network/di/ServiceModule.kt @@ -0,0 +1,17 @@ +package com.alarmy.near.network.di + +import com.alarmy.near.network.service.FriendService +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import retrofit2.Retrofit +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object ServiceModule { + @Provides + @Singleton + fun provideFriendService(retrofit: Retrofit): FriendService = retrofit.create(FriendService::class.java) +} diff --git a/Near/app/src/main/java/com/alarmy/near/network/response/FriendEntity.kt b/Near/app/src/main/java/com/alarmy/near/network/response/FriendEntity.kt new file mode 100644 index 00000000..7ec3ab3d --- /dev/null +++ b/Near/app/src/main/java/com/alarmy/near/network/response/FriendEntity.kt @@ -0,0 +1,15 @@ +package com.alarmy.near.network.response + +import kotlinx.serialization.Serializable + +@Serializable +data class FriendEntity( + val friendId: String, + val position: Int, + val source: String, + val name: String, + val imageUrl: String? = null, + val fileName: String? = null, + val checkRate: Int, + val lastContactAt: String? = null, +) diff --git a/Near/app/src/main/java/com/alarmy/near/network/service/FriendService.kt b/Near/app/src/main/java/com/alarmy/near/network/service/FriendService.kt new file mode 100644 index 00000000..a412dadc --- /dev/null +++ b/Near/app/src/main/java/com/alarmy/near/network/service/FriendService.kt @@ -0,0 +1,9 @@ +package com.alarmy.near.network.service + +import com.alarmy.near.network.response.FriendEntity +import retrofit2.http.GET + +interface FriendService { + @GET("/friend/list") + suspend fun fetchFriends(): List +} diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt index 01b8a497..56fa7f29 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt @@ -67,7 +67,7 @@ internal fun HomeRoute( onAlarmClick: () -> Unit = {}, onMyPageClick: () -> Unit = {}, ) { - val uiState = viewModel.uiStateFlow.collectAsStateWithLifecycle() + val uiState = viewModel.friendsFlow.collectAsStateWithLifecycle() HomeScreen( onContactClick = {}, onAlarmClick = {}, diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeViewModel.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeViewModel.kt index 6a0b0496..aba76bc8 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeViewModel.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeViewModel.kt @@ -2,52 +2,36 @@ package com.alarmy.near.presentation.feature.home import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.alarmy.near.data.repository.ExampleRepository +import com.alarmy.near.data.repository.FriendRepository +import com.alarmy.near.model.Friend import com.alarmy.near.presentation.feature.home.model.HomeUiState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class HomeViewModel @Inject constructor( - private val exampleRepository: ExampleRepository, + private val friendRepository: FriendRepository, ) : ViewModel() { // Example: 여러번 초기화되는 StateFlow private val _uiStateFlow = MutableStateFlow(HomeUiState.Loading) val uiStateFlow = _uiStateFlow.asStateFlow() - // Example: 한 번만 초기화되는 StateFlow - private val exampleStateFlow = - exampleRepository - .getData() - .map { - // Mapping to UIState - }.stateIn( + val friendsFlow: StateFlow> = + friendRepository + .fetchFriends() + .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), - initialValue = HomeUiState.Loading, + initialValue = emptyList(), ) - fun fetchContacts() { - viewModelScope.launch { - exampleRepository - .getData() - .catch { - // handle error - }.collect { - // updateUI - } - } - } - fun removeContact(id: Long) { // contactRepository.removeContact(id) } diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/main/NearNavHost.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/main/NearNavHost.kt index 239fa4e1..78d50a02 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/main/NearNavHost.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/main/NearNavHost.kt @@ -5,8 +5,8 @@ import androidx.compose.ui.Modifier import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import com.alarmy.near.presentation.feature.friendprofile.navigation.friendProfileNavGraph -import com.alarmy.near.presentation.feature.friendprofileedittor.navigation.RouteFriendProfileEditor import com.alarmy.near.presentation.feature.friendprofileedittor.navigation.friendProfileEditorNavGraph +import com.alarmy.near.presentation.feature.home.navigation.RouteHome import com.alarmy.near.presentation.feature.home.navigation.homeNavGraph @Composable @@ -21,7 +21,7 @@ internal fun NearNavHost( NavHost( modifier = modifier, navController = navController, - startDestination = RouteFriendProfileEditor, + startDestination = RouteHome, ) { friendProfileNavGraph(onShowErrorSnackBar = onShowSnackbar, onClickBackButton = { navController.popBackStack() diff --git a/Near/gradle/libs.versions.toml b/Near/gradle/libs.versions.toml index b0336eb5..9c13f8fc 100644 --- a/Near/gradle/libs.versions.toml +++ b/Near/gradle/libs.versions.toml @@ -12,7 +12,7 @@ composeBom = "2024.09.00" hiltVersion = "2.57" hiltNavigationVersion = "1.2.0" # Retrofit -retrofitVersion = "2.9.0" +retrofitVersion = "3.0.0" # Glide glideVersion = "4.16.0" # Room @@ -23,6 +23,8 @@ ktlintVersion = "13.0.0" navigationVersion = "2.9.2" # Kotlin Serialization kotlinSerializationVersion = "1.9.0" +# OkHttp +okHttp = "5.1.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -43,7 +45,6 @@ hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref hilt-android-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hiltVersion" } hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigationVersion" } retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofitVersion" } -retrofit-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofitVersion" } glide = { group = "com.github.bumptech.glide", name = "glide", version.ref = "glideVersion" } glide-compiler = { group = "com.github.bumptech.glide", name = "compiler", version.ref = "glideVersion" } room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "roomVersion" } @@ -52,6 +53,8 @@ room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = room-paging = { group = "androidx.room", name = "room-paging", version.ref = "roomVersion" } navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationVersion" } kotlin-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinSerializationVersion" } +logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okHttp" } +retrofit-kotlin-serialization-converter = { group = "com.squareup.retrofit2", name = "converter-kotlinx-serialization", version.ref = "retrofitVersion" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }