diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..e139682 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +do_sopt_compose \ No newline at end of file diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml index c4334ac..0c0c338 100644 --- a/.idea/deploymentTargetDropDown.xml +++ b/.idea/deploymentTargetDropDown.xml @@ -2,9 +2,6 @@ - - - diff --git a/.idea/encodings.xml b/.idea/encodings.xml index 97626ba..b4206a4 100644 --- a/.idea/encodings.xml +++ b/.idea/encodings.xml @@ -1,6 +1,7 @@ + \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index abb5cd5..a18a3d9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,6 +1,10 @@ +import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties + plugins { id("com.android.application") id("org.jetbrains.kotlin.android") + id ("kotlin-parcelize") + id("org.jetbrains.kotlin.plugin.serialization") version "1.8.0" } android { @@ -18,6 +22,9 @@ android { vectorDrawables { useSupportLibrary = true } + + buildConfigField("String", "BASE_URL", gradleLocalProperties(rootDir).getProperty("BASE_URL")) + } buildTypes { @@ -29,6 +36,7 @@ android { ) } } + compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 @@ -38,6 +46,7 @@ android { } buildFeatures { compose = true + buildConfig = true } composeOptions { kotlinCompilerExtensionVersion = "1.5.1" @@ -77,4 +86,20 @@ dependencies { implementation("org.orbit-mvi:orbit-viewmodel:4.4.0") implementation("org.orbit-mvi:orbit-compose:4.4.0") testImplementation("org.orbit-mvi:orbit-test:4.4.0") + + // openApi + implementation ("com.squareup.retrofit2:retrofit:2.9.0") + implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1") + implementation ("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0") + + implementation(platform("com.squareup.okhttp3:okhttp-bom:4.10.0")) + + implementation("com.squareup.okhttp3:okhttp") + implementation("com.squareup.okhttp3:logging-interceptor") + + implementation ("com.squareup.okhttp3:okhttp:4.11.0") + implementation ("com.squareup.okhttp3:logging-interceptor:4.10.0") + implementation ("com.squareup.retrofit2:converter-gson:2.1.0") + + implementation("io.coil-kt:coil-compose:2.6.0") } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0602202..64622d8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + + Log.d("Retrofit2", "CONNECTION INFO -> $message") + } + loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY + return loggingInterceptor + } + + private val okHttpClient = OkHttpClient.Builder() + .addInterceptor(getLogOkHttpClient()) + .build() + + fun retrofit(url: String): Retrofit = + Retrofit.Builder() + .baseUrl(url) + .client(okHttpClient) + //.addConverterFactory(Json.asConverterFactory("application/json".toMediaType())) + .addConverterFactory(GsonConverterFactory.create()) + .build() + + inline fun create(url: B): T = + retrofit(url.toString()).create(T::class.java) +} + +object ServicePool { + val apiService = ApiFactory.create(BASE_URL) +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/do_sopt_compose/data/dto/ResponsePeopleDto.kt b/app/src/main/java/org/sopt/do_sopt_compose/data/dto/ResponsePeopleDto.kt new file mode 100644 index 0000000..1ecf091 --- /dev/null +++ b/app/src/main/java/org/sopt/do_sopt_compose/data/dto/ResponsePeopleDto.kt @@ -0,0 +1,43 @@ +package org.sopt.do_sopt_compose.data.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + + +@Serializable +data class ResponsePeopleDto( + @SerialName("page") + val page: Int, + @SerialName("per_page") + val perPage: Int, + @SerialName("total") + val total: Int, + @SerialName("total_pages") + val totalPages: Int, + @SerialName("data") + val data: List, + @SerialName("support") + val support: Support, +) + +@Serializable +data class User( + @SerialName("id") + val id: Int, + @SerialName("email") + val email: String, + @SerialName("first_name") + val firstName: String, + @SerialName("last_name") + val lastName: String, + @SerialName("avatar") + val avatar: String, +) + +@Serializable +data class Support( + @SerialName("url") + val url: String, + @SerialName("text") + val text: String, +) diff --git a/app/src/main/java/org/sopt/do_sopt_compose/data/repository/PeopleRepository.kt b/app/src/main/java/org/sopt/do_sopt_compose/data/repository/PeopleRepository.kt new file mode 100644 index 0000000..4e03709 --- /dev/null +++ b/app/src/main/java/org/sopt/do_sopt_compose/data/repository/PeopleRepository.kt @@ -0,0 +1,9 @@ +package org.sopt.do_sopt_compose.data.repository + +import org.sopt.do_sopt_compose.data.ServicePool + +class PeopleRepository { + + private val apiService = ServicePool.apiService + suspend fun getUsers(page: Int) = apiService.getUsers(page, 10) +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/do_sopt_compose/data/service/PeopleApiService.kt b/app/src/main/java/org/sopt/do_sopt_compose/data/service/PeopleApiService.kt new file mode 100644 index 0000000..477d724 --- /dev/null +++ b/app/src/main/java/org/sopt/do_sopt_compose/data/service/PeopleApiService.kt @@ -0,0 +1,13 @@ +package org.sopt.do_sopt_compose.data.service + +import org.sopt.do_sopt_compose.data.dto.ResponsePeopleDto +import retrofit2.http.GET +import retrofit2.http.Query + +interface PeopleApiService { + @GET("users") + suspend fun getUsers( + @Query("page") page: Int, + @Query("per_page") perPage: Int, + ): ResponsePeopleDto +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/do_sopt_compose/navigation/MainNavigation.kt b/app/src/main/java/org/sopt/do_sopt_compose/navigation/MainNavigation.kt index 87d4d3a..470bc45 100644 --- a/app/src/main/java/org/sopt/do_sopt_compose/navigation/MainNavigation.kt +++ b/app/src/main/java/org/sopt/do_sopt_compose/navigation/MainNavigation.kt @@ -6,7 +6,8 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable -import org.sopt.do_sopt_compose.ui.pages.doandroid.DoAndroid +import org.sopt.do_sopt_compose.ui.pages.doandroid.DoAndroidScreen +import org.sopt.do_sopt_compose.ui.pages.doandroid.DoAndroidViewModel import org.sopt.do_sopt_compose.ui.pages.home.HomePage import org.sopt.do_sopt_compose.ui.pages.login.LoginPage import org.sopt.do_sopt_compose.ui.pages.mypage.MainPage @@ -50,7 +51,7 @@ private fun NavGraphBuilder.addMain(navController: NavController) { private fun NavGraphBuilder.addDo(navController: NavController) { composable(route = BottomNaviItems.Do.route) { - DoAndroid( + DoAndroidScreen( navController = navController, ) } diff --git a/app/src/main/java/org/sopt/do_sopt_compose/ui/pages/doandroid/DoAndroid.kt b/app/src/main/java/org/sopt/do_sopt_compose/ui/pages/doandroid/DoAndroid.kt deleted file mode 100644 index 30c5a01..0000000 --- a/app/src/main/java/org/sopt/do_sopt_compose/ui/pages/doandroid/DoAndroid.kt +++ /dev/null @@ -1,29 +0,0 @@ -package org.sopt.do_sopt_compose.ui.pages.doandroid - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.navigation.NavController -import org.sopt.do_sopt_compose.navigation.BottomNavigation - -@Composable -fun DoAndroid( - navController: NavController, -) { - Scaffold( - bottomBar = { BottomNavigation(navController = navController) }, - content = { - Box( - modifier = Modifier - .fillMaxSize() - .padding(it), - ) { - Text(text = "Do") - } - } - ) -} diff --git a/app/src/main/java/org/sopt/do_sopt_compose/ui/pages/doandroid/DoAndroidScreen.kt b/app/src/main/java/org/sopt/do_sopt_compose/ui/pages/doandroid/DoAndroidScreen.kt new file mode 100644 index 0000000..941e6c6 --- /dev/null +++ b/app/src/main/java/org/sopt/do_sopt_compose/ui/pages/doandroid/DoAndroidScreen.kt @@ -0,0 +1,101 @@ +package org.sopt.do_sopt_compose.ui.pages.doandroid + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Card +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import coil.compose.rememberAsyncImagePainter +import coil.request.ImageRequest +import coil.size.Size +import org.sopt.do_sopt_compose.R +import org.sopt.do_sopt_compose.data.dto.User +import org.sopt.do_sopt_compose.navigation.BottomNavigation + +@Composable +fun DoAndroidScreen( + navController: NavController, +) { + val viewModel: DoAndroidViewModel = viewModel() + val usersState = viewModel.users.collectAsState() + + Scaffold( + bottomBar = { BottomNavigation(navController = navController) }, + content = { + Box( + modifier = Modifier + .fillMaxSize() + .padding(it), + ) { + UserList(users = usersState.value) + } + } + ) +} + +@Composable +fun UserList(users: List) { + LazyColumn { + items(users) { user -> + UserItem(user) + } + } +} + +@Composable +fun UserItem(user: User) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) { + Row( + modifier = Modifier.padding(8.dp) + ) { + // 프로필 이미지 표시 + Image( + painter = + rememberAsyncImagePainter( + ImageRequest.Builder(LocalContext.current).data(data = user.avatar) + .size(Size.ORIGINAL) + .apply(block = fun ImageRequest.Builder.() { + placeholder(R.drawable.profile_background) + }).build() + ), + contentDescription = "Profile Picture", + modifier = Modifier + .size(64.dp) + .padding(end = 16.dp) + ) + Column { + Text( + text = "ID: ${user.id}", + modifier = Modifier.padding(bottom = 4.dp) + ) + Text( + text = "Name: ${user.firstName} ${user.lastName}", + modifier = Modifier.padding(bottom = 4.dp) + ) + Text( + text = "Email: ${user.email}", + modifier = Modifier.padding(bottom = 4.dp) + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/do_sopt_compose/ui/pages/doandroid/DoAndroidSideEffect.kt b/app/src/main/java/org/sopt/do_sopt_compose/ui/pages/doandroid/DoAndroidSideEffect.kt new file mode 100644 index 0000000..695519c --- /dev/null +++ b/app/src/main/java/org/sopt/do_sopt_compose/ui/pages/doandroid/DoAndroidSideEffect.kt @@ -0,0 +1,5 @@ +package org.sopt.do_sopt_compose.ui.pages.doandroid + +sealed class DoAndroidSideEffect { + data class ToastMessage(val message: String) : DoAndroidSideEffect() +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/do_sopt_compose/ui/pages/doandroid/DoAndroidState.kt b/app/src/main/java/org/sopt/do_sopt_compose/ui/pages/doandroid/DoAndroidState.kt new file mode 100644 index 0000000..c4fec1c --- /dev/null +++ b/app/src/main/java/org/sopt/do_sopt_compose/ui/pages/doandroid/DoAndroidState.kt @@ -0,0 +1,7 @@ +package org.sopt.do_sopt_compose.ui.pages.doandroid + +import org.sopt.do_sopt_compose.ui.UiStatus + +data class DoAndroidState( + val uiStatus: UiStatus = UiStatus.Loading, +) diff --git a/app/src/main/java/org/sopt/do_sopt_compose/ui/pages/doandroid/DoAndroidViewModel.kt b/app/src/main/java/org/sopt/do_sopt_compose/ui/pages/doandroid/DoAndroidViewModel.kt new file mode 100644 index 0000000..9dedae6 --- /dev/null +++ b/app/src/main/java/org/sopt/do_sopt_compose/ui/pages/doandroid/DoAndroidViewModel.kt @@ -0,0 +1,55 @@ +package org.sopt.do_sopt_compose.ui.pages.doandroid + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.syntax.simple.intent +import org.orbitmvi.orbit.syntax.simple.postSideEffect +import org.orbitmvi.orbit.syntax.simple.reduce +import org.orbitmvi.orbit.viewmodel.container +import org.sopt.do_sopt_compose.data.repository.PeopleRepository +import org.sopt.do_sopt_compose.data.dto.User +import org.sopt.do_sopt_compose.ui.UiStatus + +class DoAndroidViewModel : ContainerHost, ViewModel() { + + private val peopleRepository = PeopleRepository() + private val _users = MutableStateFlow>(emptyList()) + val users = _users.asStateFlow() + override val container = container(DoAndroidState()) + + init { + loadPeopleData(1) + } + + private fun loadPeopleData(page: Int) { + viewModelScope.launch { + intent { + reduce { + state.copy(uiStatus = UiStatus.Loading) + } + } + try { + _users.emit(peopleRepository.getUsers(page).data) + intent { + reduce { + state.copy(uiStatus = UiStatus.Success) + } + postSideEffect(DoAndroidSideEffect.ToastMessage("데이터 로드 성공")) + } + } catch (e: Exception) { + _users.value = emptyList() + intent { + reduce { + state.copy(uiStatus = UiStatus.Fail) + } + postSideEffect(DoAndroidSideEffect.ToastMessage("데이터 로드 실패")) + } + e.printStackTrace() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/profile_background.xml b/app/src/main/res/drawable/profile_background.xml new file mode 100644 index 0000000..e416bbc --- /dev/null +++ b/app/src/main/res/drawable/profile_background.xml @@ -0,0 +1,9 @@ + + +