diff --git a/app/build.gradle b/app/build.gradle index b04dc4c..3fce873 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,6 +8,9 @@ plugins { id 'org.jetbrains.kotlin.plugin.serialization' version '1.7.10' } +Properties properties = new Properties() +properties.load(project.rootProject.file('local.properties').newDataInputStream()) + android { namespace 'org.android.go.sopt' compileSdk 33 @@ -20,6 +23,9 @@ android { versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + buildConfigField "String", "BASE_URL", properties['base.url'] + buildConfigField "String", "REQRES_URL", properties['reqres.url'] } buildTypes { @@ -81,6 +87,11 @@ dependencies { implementation "com.google.code.gson:gson:2.10.1" implementation "com.squareup.retrofit2:retrofit:2.9.0" + // okhttp + implementation "com.squareup.okhttp3:okhttp" + implementation "com.squareup.okhttp3:okhttp-bom:4.10.0" + implementation "com.squareup.okhttp3:logging-interceptor" + // coil implementation "io.coil-kt:coil:2.3.0" diff --git a/app/src/main/java/org/android/go/sopt/data/entity/MockRepoDto.kt b/app/src/main/java/org/android/go/sopt/data/entity/local/MockRepoDto.kt similarity index 90% rename from app/src/main/java/org/android/go/sopt/data/entity/MockRepoDto.kt rename to app/src/main/java/org/android/go/sopt/data/entity/local/MockRepoDto.kt index 6d6ffc3..82431b8 100644 --- a/app/src/main/java/org/android/go/sopt/data/entity/MockRepoDto.kt +++ b/app/src/main/java/org/android/go/sopt/data/entity/local/MockRepoDto.kt @@ -1,4 +1,4 @@ -package org.android.go.sopt.data.entity +package org.android.go.sopt.data.entity.local import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -10,6 +10,7 @@ data class MockRepoDto( val allowForking: Boolean, @SerialName("archive_url") val archiveUrl: String, + @SerialName("archived") val archived: Boolean, @SerialName("assignees_url") val assigneesUrl: String, @@ -37,13 +38,17 @@ data class MockRepoDto( val defaultBranch: String, @SerialName("deployments_url") val deploymentsUrl: String, + @SerialName("description") val description: String, + @SerialName("disabled") val disabled: Boolean, @SerialName("downloads_url") val downloadsUrl: String, @SerialName("events_url") val eventsUrl: String, + @SerialName("fork") val fork: Boolean, + @SerialName("forks") val forks: Int, @SerialName("forks_count") val forksCount: Int, @@ -71,11 +76,13 @@ data class MockRepoDto( val hasProjects: Boolean, @SerialName("has_wiki") val hasWiki: Boolean, + @SerialName("homepage") val homepage: String, @SerialName("hooks_url") val hooksUrl: String, @SerialName("html_url") val htmlUrl: String, + @SerialName("id") val id: Int, @SerialName("is_template") val isTemplate: Boolean, @@ -89,9 +96,11 @@ data class MockRepoDto( val keysUrl: String, @SerialName("labels_url") val labelsUrl: String, + @SerialName("language") val language: String?, @SerialName("languages_url") val languagesUrl: String, + @SerialName("license") val license: String?, @SerialName("merges_url") val mergesUrl: String, @@ -99,6 +108,7 @@ data class MockRepoDto( val milestonesUrl: String, @SerialName("mirror_url") val mirrorUrl: String?, + @SerialName("name") val name: String, @SerialName("node_id") val nodeId: String, @@ -108,6 +118,7 @@ data class MockRepoDto( val openIssues: Int, @SerialName("open_issues_count") val openIssuesCount: Int, + @SerialName("owner") val owner: Owner, @SerialName("private") val isPrivate: Boolean, @@ -117,6 +128,7 @@ data class MockRepoDto( val pushedAt: String, @SerialName("releases_url") val releasesUrl: String, + @SerialName("size") val size: Int, @SerialName("ssh_url") val sshUrl: String, @@ -136,13 +148,17 @@ data class MockRepoDto( val tagsUrl: String, @SerialName("teams_url") val teamsUrl: String, + @SerialName("topics") val topics: List, @SerialName("trees_url") val treesUrl: String, @SerialName("updated_at") val updatedAt: String, + @SerialName("url") val url: String, + @SerialName("visibility") val visibility: String, + @SerialName("watchers") val watchers: Int, @SerialName("watchers_count") val watchersCount: Int, @@ -165,7 +181,9 @@ data class MockRepoDto( val gravatarId: String, @SerialName("html_url") val htmlUrl: String, + @SerialName("id") val id: Int, + @SerialName("login") val login: String, @SerialName("node_id") val nodeId: String, @@ -181,7 +199,9 @@ data class MockRepoDto( val starredUrl: String, @SerialName("subscriptions_url") val subscriptionsUrl: String, + @SerialName("type") val type: String, + @SerialName("url") val url: String, ) diff --git a/app/src/main/java/org/android/go/sopt/data/entity/remote/request/RequestPostSignInDto.kt b/app/src/main/java/org/android/go/sopt/data/entity/remote/request/RequestPostSignInDto.kt new file mode 100644 index 0000000..bc7a894 --- /dev/null +++ b/app/src/main/java/org/android/go/sopt/data/entity/remote/request/RequestPostSignInDto.kt @@ -0,0 +1,12 @@ +package org.android.go.sopt.data.entity.remote.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RequestPostSignInDto( + @SerialName("id") + val id: String, + @SerialName("password") + val password: String, +) diff --git a/app/src/main/java/org/android/go/sopt/data/entity/remote/request/RequestPostSignUpDto.kt b/app/src/main/java/org/android/go/sopt/data/entity/remote/request/RequestPostSignUpDto.kt new file mode 100644 index 0000000..49511d8 --- /dev/null +++ b/app/src/main/java/org/android/go/sopt/data/entity/remote/request/RequestPostSignUpDto.kt @@ -0,0 +1,16 @@ +package org.android.go.sopt.data.entity.remote.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RequestPostSignUpDto( + @SerialName("id") + val id: String, + @SerialName("password") + val password: String, + @SerialName("name") + val name: String, + @SerialName("skill") + val skill: String, +) diff --git a/app/src/main/java/org/android/go/sopt/data/entity/remote/response/ResponseGetFollowerListDto.kt b/app/src/main/java/org/android/go/sopt/data/entity/remote/response/ResponseGetFollowerListDto.kt new file mode 100644 index 0000000..0eefc40 --- /dev/null +++ b/app/src/main/java/org/android/go/sopt/data/entity/remote/response/ResponseGetFollowerListDto.kt @@ -0,0 +1,52 @@ +package org.android.go.sopt.data.entity.remote.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import org.android.go.sopt.domain.model.Follower + +@Serializable +data class ResponseGetFollowerListDto( + @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 Follower( + @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, + ) + + fun toFollower() = data.map { follower -> + Follower( + id = follower.id, + name = "${follower.firstName} ${follower.lastName}", + profile = follower.avatar, + email = follower.email, + ) + } +} diff --git a/app/src/main/java/org/android/go/sopt/data/entity/remote/response/ResponsePostSignInDto.kt b/app/src/main/java/org/android/go/sopt/data/entity/remote/response/ResponsePostSignInDto.kt new file mode 100644 index 0000000..01e6ed2 --- /dev/null +++ b/app/src/main/java/org/android/go/sopt/data/entity/remote/response/ResponsePostSignInDto.kt @@ -0,0 +1,14 @@ +package org.android.go.sopt.data.entity.remote.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponsePostSignInDto( + @SerialName("id") + val id: String, + @SerialName("name") + val name: String, + @SerialName("skill") + val skill: String?, +) diff --git a/app/src/main/java/org/android/go/sopt/data/entity/remote/response/ResponsePostSignUpDto.kt b/app/src/main/java/org/android/go/sopt/data/entity/remote/response/ResponsePostSignUpDto.kt new file mode 100644 index 0000000..52c3fdf --- /dev/null +++ b/app/src/main/java/org/android/go/sopt/data/entity/remote/response/ResponsePostSignUpDto.kt @@ -0,0 +1,12 @@ +package org.android.go.sopt.data.entity.remote.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponsePostSignUpDto( + @SerialName("name") + val name: String, + @SerialName("skill") + val skill: String, +) diff --git a/app/src/main/java/org/android/go/sopt/data/entity/remote/response/wrapper/BaseResponse.kt b/app/src/main/java/org/android/go/sopt/data/entity/remote/response/wrapper/BaseResponse.kt new file mode 100644 index 0000000..0a2ac9f --- /dev/null +++ b/app/src/main/java/org/android/go/sopt/data/entity/remote/response/wrapper/BaseResponse.kt @@ -0,0 +1,14 @@ +package org.android.go.sopt.data.entity.remote.response.wrapper + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class BaseResponse( + @SerialName("status") + val status: Int, + @SerialName("message") + val message: String, + @SerialName("data") + val data: T? = null, +) diff --git a/app/src/main/java/org/android/go/sopt/data/repository/AuthRepositoryImpl.kt b/app/src/main/java/org/android/go/sopt/data/repository/AuthRepositoryImpl.kt index c25b041..5ccc43d 100644 --- a/app/src/main/java/org/android/go/sopt/data/repository/AuthRepositoryImpl.kt +++ b/app/src/main/java/org/android/go/sopt/data/repository/AuthRepositoryImpl.kt @@ -1,26 +1,42 @@ package org.android.go.sopt.data.repository -import org.android.go.sopt.data.source.LocalPrefDataSource +import org.android.go.sopt.data.entity.remote.request.RequestPostSignInDto +import org.android.go.sopt.data.entity.remote.request.RequestPostSignUpDto +import org.android.go.sopt.data.entity.remote.response.ResponsePostSignInDto +import org.android.go.sopt.data.entity.remote.response.ResponsePostSignUpDto +import org.android.go.sopt.data.source.local.SharedPrefDataSource +import org.android.go.sopt.data.source.remote.AuthDataSource import org.android.go.sopt.domain.model.User import org.android.go.sopt.domain.repository.AuthRepository import javax.inject.Inject class AuthRepositoryImpl @Inject constructor( - private val localPrefDataSource: LocalPrefDataSource, + private val authDataSource: AuthDataSource, + private val sharedPrefDataSource: SharedPrefDataSource, ) : AuthRepository { + override suspend fun postSignup(requestPostSignUpDto: RequestPostSignUpDto): Result = + runCatching { + authDataSource.postSignup(requestPostSignUpDto).data + } + + override suspend fun postSignin(requestPostSignInDto: RequestPostSignInDto): Result = + runCatching { + authDataSource.postSignIn(requestPostSignInDto).data + } + override fun setAutoLogin(isAutoLogin: Boolean) { - localPrefDataSource.isAutoLogin = isAutoLogin + sharedPrefDataSource.isAutoLogin = isAutoLogin } - override fun getAutoLogin(): Boolean = localPrefDataSource.isAutoLogin + override fun getAutoLogin(): Boolean = sharedPrefDataSource.isAutoLogin override fun setSignedUpUser(user: User) { - localPrefDataSource.signedUpUser = user + sharedPrefDataSource.signedUpUser = user } - override fun getSignedUpUser(): User? = localPrefDataSource.signedUpUser + override fun getSignedUpUser(): User? = sharedPrefDataSource.signedUpUser override fun clearLocalPref() { - localPrefDataSource.clearLocalPref() + sharedPrefDataSource.clearLocalPref() } } diff --git a/app/src/main/java/org/android/go/sopt/data/repository/FollowerRepositoryImpl.kt b/app/src/main/java/org/android/go/sopt/data/repository/FollowerRepositoryImpl.kt new file mode 100644 index 0000000..066452f --- /dev/null +++ b/app/src/main/java/org/android/go/sopt/data/repository/FollowerRepositoryImpl.kt @@ -0,0 +1,15 @@ +package org.android.go.sopt.data.repository + +import org.android.go.sopt.data.source.remote.FollowerDataSource +import org.android.go.sopt.domain.model.Follower +import org.android.go.sopt.domain.repository.FollowerRepository +import javax.inject.Inject + +class FollowerRepositoryImpl @Inject constructor( + private val followerDataSource: FollowerDataSource, +) : FollowerRepository { + override suspend fun getFollowerList(page: Int): Result> = + runCatching { + followerDataSource.getFollowerList(page).toFollower() + } +} diff --git a/app/src/main/java/org/android/go/sopt/data/repository/RepoRepositoryImpl.kt b/app/src/main/java/org/android/go/sopt/data/repository/RepoRepositoryImpl.kt index 733d6e1..25cf5d4 100644 --- a/app/src/main/java/org/android/go/sopt/data/repository/RepoRepositoryImpl.kt +++ b/app/src/main/java/org/android/go/sopt/data/repository/RepoRepositoryImpl.kt @@ -1,6 +1,6 @@ package org.android.go.sopt.data.repository -import org.android.go.sopt.data.source.RepoDataSource +import org.android.go.sopt.data.source.local.RepoDataSource import org.android.go.sopt.domain.model.Repo import org.android.go.sopt.domain.repository.RepoRepository import javax.inject.Inject diff --git a/app/src/main/java/org/android/go/sopt/data/service/AuthService.kt b/app/src/main/java/org/android/go/sopt/data/service/AuthService.kt new file mode 100644 index 0000000..233e16a --- /dev/null +++ b/app/src/main/java/org/android/go/sopt/data/service/AuthService.kt @@ -0,0 +1,21 @@ +package org.android.go.sopt.data.service + +import org.android.go.sopt.data.entity.remote.request.RequestPostSignInDto +import org.android.go.sopt.data.entity.remote.request.RequestPostSignUpDto +import org.android.go.sopt.data.entity.remote.response.ResponsePostSignInDto +import org.android.go.sopt.data.entity.remote.response.ResponsePostSignUpDto +import org.android.go.sopt.data.entity.remote.response.wrapper.BaseResponse +import retrofit2.http.Body +import retrofit2.http.POST + +interface AuthService { + @POST("sign-up") + suspend fun postSignUp( + @Body requestPostSignUpDto: RequestPostSignUpDto, + ): BaseResponse + + @POST("sign-in") + suspend fun postSignIn( + @Body requestPostSignInDto: RequestPostSignInDto, + ): BaseResponse +} diff --git a/app/src/main/java/org/android/go/sopt/data/service/FollowerService.kt b/app/src/main/java/org/android/go/sopt/data/service/FollowerService.kt new file mode 100644 index 0000000..125bd00 --- /dev/null +++ b/app/src/main/java/org/android/go/sopt/data/service/FollowerService.kt @@ -0,0 +1,12 @@ +package org.android.go.sopt.data.service + +import org.android.go.sopt.data.entity.remote.response.ResponseGetFollowerListDto +import retrofit2.http.GET +import retrofit2.http.Query + +interface FollowerService { + @GET("users") + suspend fun getFollowerList( + @Query("page") page: Int, + ): ResponseGetFollowerListDto +} diff --git a/app/src/main/java/org/android/go/sopt/data/source/RepoDataSource.kt b/app/src/main/java/org/android/go/sopt/data/source/local/RepoDataSource.kt similarity index 84% rename from app/src/main/java/org/android/go/sopt/data/source/RepoDataSource.kt rename to app/src/main/java/org/android/go/sopt/data/source/local/RepoDataSource.kt index 0da411f..377c9dd 100644 --- a/app/src/main/java/org/android/go/sopt/data/source/RepoDataSource.kt +++ b/app/src/main/java/org/android/go/sopt/data/source/local/RepoDataSource.kt @@ -1,8 +1,8 @@ -package org.android.go.sopt.data.source +package org.android.go.sopt.data.source.local import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json -import org.android.go.sopt.data.entity.MockRepoDto +import org.android.go.sopt.data.entity.local.MockRepoDto import org.android.go.sopt.util.AssetLoader import javax.inject.Inject diff --git a/app/src/main/java/org/android/go/sopt/data/source/LocalPrefDataSource.kt b/app/src/main/java/org/android/go/sopt/data/source/local/SharedPrefDataSource.kt similarity index 87% rename from app/src/main/java/org/android/go/sopt/data/source/LocalPrefDataSource.kt rename to app/src/main/java/org/android/go/sopt/data/source/local/SharedPrefDataSource.kt index def0dc7..a189381 100644 --- a/app/src/main/java/org/android/go/sopt/data/source/LocalPrefDataSource.kt +++ b/app/src/main/java/org/android/go/sopt/data/source/local/SharedPrefDataSource.kt @@ -1,4 +1,4 @@ -package org.android.go.sopt.data.source +package org.android.go.sopt.data.source.local import android.content.SharedPreferences import androidx.core.content.edit @@ -9,7 +9,7 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class LocalPrefDataSource @Inject constructor( +class SharedPrefDataSource @Inject constructor( private val prefs: SharedPreferences, ) { var isAutoLogin: Boolean @@ -30,9 +30,7 @@ class LocalPrefDataSource @Inject constructor( } } - fun clearLocalPref() { - prefs.edit { clear() } - } + fun clearLocalPref() = prefs.edit { clear() } companion object { const val PREF_IS_AUTO_LOGIN = "IS_AUTO_LOGIN" diff --git a/app/src/main/java/org/android/go/sopt/data/source/remote/AuthDataSource.kt b/app/src/main/java/org/android/go/sopt/data/source/remote/AuthDataSource.kt new file mode 100644 index 0000000..1f993cd --- /dev/null +++ b/app/src/main/java/org/android/go/sopt/data/source/remote/AuthDataSource.kt @@ -0,0 +1,23 @@ +package org.android.go.sopt.data.source.remote + +import org.android.go.sopt.data.entity.remote.request.RequestPostSignInDto +import org.android.go.sopt.data.entity.remote.request.RequestPostSignUpDto +import org.android.go.sopt.data.entity.remote.response.ResponsePostSignInDto +import org.android.go.sopt.data.entity.remote.response.ResponsePostSignUpDto +import org.android.go.sopt.data.entity.remote.response.wrapper.BaseResponse +import org.android.go.sopt.data.service.AuthService +import javax.inject.Inject + +class AuthDataSource @Inject constructor( + private val authService: AuthService, +) { + suspend fun postSignup( + requestPostSignUpDto: RequestPostSignUpDto, + ): BaseResponse = + authService.postSignUp(requestPostSignUpDto) + + suspend fun postSignIn( + requestPostSignInDto: RequestPostSignInDto, + ): BaseResponse = + authService.postSignIn(requestPostSignInDto) +} diff --git a/app/src/main/java/org/android/go/sopt/data/source/remote/FollowerDataSource.kt b/app/src/main/java/org/android/go/sopt/data/source/remote/FollowerDataSource.kt new file mode 100644 index 0000000..ea68131 --- /dev/null +++ b/app/src/main/java/org/android/go/sopt/data/source/remote/FollowerDataSource.kt @@ -0,0 +1,12 @@ +package org.android.go.sopt.data.source.remote + +import org.android.go.sopt.data.entity.remote.response.ResponseGetFollowerListDto +import org.android.go.sopt.data.service.FollowerService +import javax.inject.Inject + +class FollowerDataSource @Inject constructor( + private val followerService: FollowerService, +) { + suspend fun getFollowerList(page: Int): ResponseGetFollowerListDto = + followerService.getFollowerList(page) +} diff --git a/app/src/main/java/org/android/go/sopt/di/RepositoryModule.kt b/app/src/main/java/org/android/go/sopt/di/RepositoryModule.kt index c8c39b4..7ccc9d1 100644 --- a/app/src/main/java/org/android/go/sopt/di/RepositoryModule.kt +++ b/app/src/main/java/org/android/go/sopt/di/RepositoryModule.kt @@ -5,8 +5,10 @@ import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import org.android.go.sopt.data.repository.AuthRepositoryImpl +import org.android.go.sopt.data.repository.FollowerRepositoryImpl import org.android.go.sopt.data.repository.RepoRepositoryImpl import org.android.go.sopt.domain.repository.AuthRepository +import org.android.go.sopt.domain.repository.FollowerRepository import org.android.go.sopt.domain.repository.RepoRepository import javax.inject.Singleton @@ -24,4 +26,10 @@ abstract class RepositoryModule { abstract fun bindsRepoRepository( repoRepositoryImpl: RepoRepositoryImpl, ): RepoRepository + + @Binds + @Singleton + abstract fun bindsFollowerRepository( + followerRepositoryImpl: FollowerRepositoryImpl, + ): FollowerRepository } diff --git a/app/src/main/java/org/android/go/sopt/di/RetrofitModule.kt b/app/src/main/java/org/android/go/sopt/di/RetrofitModule.kt new file mode 100644 index 0000000..5d486f0 --- /dev/null +++ b/app/src/main/java/org/android/go/sopt/di/RetrofitModule.kt @@ -0,0 +1,61 @@ +package org.android.go.sopt.di + +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +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 org.android.go.sopt.BuildConfig.BASE_URL +import org.android.go.sopt.BuildConfig.REQRES_URL +import org.android.go.sopt.util.type.BaseUrlType +import retrofit2.Retrofit +import javax.inject.Qualifier +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object RetrofitModule { + private const val APPLICATION_JSON = "application/json" + + @Provides + @Singleton + fun providesOkHttpClient(): OkHttpClient = + OkHttpClient.Builder() + .addInterceptor( + HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + }, + ) + .build() + + @Provides + @Singleton + @Retrofit2(BaseUrlType.SOPT) + fun providesSoptRetrofit( + client: OkHttpClient, + ): Retrofit = + Retrofit.Builder() + .baseUrl(BASE_URL) + .addConverterFactory(Json.asConverterFactory(APPLICATION_JSON.toMediaType())) + .client(client) + .build() + + @Provides + @Singleton + @Retrofit2(BaseUrlType.REQRES) + fun providesReqresRetrofit( + client: OkHttpClient, + ): Retrofit = + Retrofit.Builder() + .baseUrl(REQRES_URL) + .addConverterFactory(Json.asConverterFactory(APPLICATION_JSON.toMediaType())) + .client(client) + .build() + + @Qualifier + annotation class Retrofit2(val baseUrlType: BaseUrlType) +} diff --git a/app/src/main/java/org/android/go/sopt/di/ServiceModule.kt b/app/src/main/java/org/android/go/sopt/di/ServiceModule.kt new file mode 100644 index 0000000..8f58473 --- /dev/null +++ b/app/src/main/java/org/android/go/sopt/di/ServiceModule.kt @@ -0,0 +1,26 @@ +package org.android.go.sopt.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import org.android.go.sopt.data.service.AuthService +import org.android.go.sopt.data.service.FollowerService +import org.android.go.sopt.di.RetrofitModule.Retrofit2 +import org.android.go.sopt.util.type.BaseUrlType +import retrofit2.Retrofit +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object ServiceModule { + @Provides + @Singleton + fun providesAuthService(@Retrofit2(BaseUrlType.SOPT) retrofit: Retrofit): AuthService = + retrofit.create(AuthService::class.java) + + @Provides + @Singleton + fun providesFollowerService(@Retrofit2(BaseUrlType.REQRES) retrofit: Retrofit): FollowerService = + retrofit.create(FollowerService::class.java) +} diff --git a/app/src/main/java/org/android/go/sopt/domain/model/Follower.kt b/app/src/main/java/org/android/go/sopt/domain/model/Follower.kt new file mode 100644 index 0000000..a393d88 --- /dev/null +++ b/app/src/main/java/org/android/go/sopt/domain/model/Follower.kt @@ -0,0 +1,8 @@ +package org.android.go.sopt.domain.model + +data class Follower( + val id: Int, + val name: String, + val profile: String, + val email: String, +) diff --git a/app/src/main/java/org/android/go/sopt/domain/model/User.kt b/app/src/main/java/org/android/go/sopt/domain/model/User.kt index 6fc5a11..358915a 100644 --- a/app/src/main/java/org/android/go/sopt/domain/model/User.kt +++ b/app/src/main/java/org/android/go/sopt/domain/model/User.kt @@ -2,14 +2,11 @@ package org.android.go.sopt.domain.model import android.os.Parcelable import kotlinx.parcelize.Parcelize -import org.android.go.sopt.util.type.MBTI -import org.android.go.sopt.util.type.MBTI.NONE @Parcelize data class User( val id: String = "", val pwd: String = "", - val name: String? = "", + val name: String = "익명", val specialty: String? = "", - val mbti: MBTI? = NONE, ) : Parcelable diff --git a/app/src/main/java/org/android/go/sopt/domain/repository/AuthRepository.kt b/app/src/main/java/org/android/go/sopt/domain/repository/AuthRepository.kt index b8b043c..319a4da 100644 --- a/app/src/main/java/org/android/go/sopt/domain/repository/AuthRepository.kt +++ b/app/src/main/java/org/android/go/sopt/domain/repository/AuthRepository.kt @@ -1,8 +1,16 @@ package org.android.go.sopt.domain.repository +import org.android.go.sopt.data.entity.remote.request.RequestPostSignInDto +import org.android.go.sopt.data.entity.remote.request.RequestPostSignUpDto +import org.android.go.sopt.data.entity.remote.response.ResponsePostSignInDto +import org.android.go.sopt.data.entity.remote.response.ResponsePostSignUpDto import org.android.go.sopt.domain.model.User interface AuthRepository { + suspend fun postSignup(requestPostSignUpDto: RequestPostSignUpDto): Result + + suspend fun postSignin(requestPostSignInDto: RequestPostSignInDto): Result + fun setAutoLogin(isAutoLogin: Boolean) fun getAutoLogin(): Boolean diff --git a/app/src/main/java/org/android/go/sopt/domain/repository/FollowerRepository.kt b/app/src/main/java/org/android/go/sopt/domain/repository/FollowerRepository.kt new file mode 100644 index 0000000..8e873d4 --- /dev/null +++ b/app/src/main/java/org/android/go/sopt/domain/repository/FollowerRepository.kt @@ -0,0 +1,7 @@ +package org.android.go.sopt.domain.repository + +import org.android.go.sopt.domain.model.Follower + +interface FollowerRepository { + suspend fun getFollowerList(page: Int): Result> +} diff --git a/app/src/main/java/org/android/go/sopt/presentation/login/LoginActivity.kt b/app/src/main/java/org/android/go/sopt/presentation/login/LoginActivity.kt index 550535a..a8d7593 100644 --- a/app/src/main/java/org/android/go/sopt/presentation/login/LoginActivity.kt +++ b/app/src/main/java/org/android/go/sopt/presentation/login/LoginActivity.kt @@ -2,23 +2,21 @@ package org.android.go.sopt.presentation.login import android.app.Activity import android.content.Intent -import android.content.Intent.EXTRA_USER import android.os.Bundle import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import dagger.hilt.android.AndroidEntryPoint import org.android.go.sopt.R import org.android.go.sopt.databinding.ActivityLoginBinding -import org.android.go.sopt.domain.model.User import org.android.go.sopt.presentation.main.MainActivity import org.android.go.sopt.presentation.signup.SignupActivity -import org.android.go.sopt.util.UiState.Failure -import org.android.go.sopt.util.UiState.Success import org.android.go.sopt.util.binding.BindingActivity -import org.android.go.sopt.util.extension.getCompatibleParcelableExtra import org.android.go.sopt.util.extension.setOnSingleClickListener import org.android.go.sopt.util.extension.showSnackbar import org.android.go.sopt.util.extension.showToast +import org.android.go.sopt.util.state.RemoteUiState.Error +import org.android.go.sopt.util.state.RemoteUiState.Failure +import org.android.go.sopt.util.state.RemoteUiState.Success @AndroidEntryPoint class LoginActivity : BindingActivity(R.layout.activity_login) { @@ -36,11 +34,7 @@ class LoginActivity : BindingActivity(R.layout.activity_lo val signupResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == Activity.RESULT_OK) { - val resultData = result.data ?: return@registerForActivityResult - resultData.getCompatibleParcelableExtra(EXTRA_USER)?.let { user -> - viewModel.setSavedUser(user) - showSnackbar(binding.root, getString(R.string.login_signup_success_msg)) - } + showSnackbar(binding.root, getString(R.string.login_signup_success_msg)) } } @@ -54,6 +48,7 @@ class LoginActivity : BindingActivity(R.layout.activity_lo when (state) { is Success -> navigateToMainScreen() is Failure -> showSnackbar(binding.root, getString(R.string.wrong_input_msg)) + is Error -> showSnackbar(binding.root, getString(R.string.unknown_error_msg)) } } } diff --git a/app/src/main/java/org/android/go/sopt/presentation/login/LoginViewModel.kt b/app/src/main/java/org/android/go/sopt/presentation/login/LoginViewModel.kt index d3bf1cd..fcaa003 100644 --- a/app/src/main/java/org/android/go/sopt/presentation/login/LoginViewModel.kt +++ b/app/src/main/java/org/android/go/sopt/presentation/login/LoginViewModel.kt @@ -3,48 +3,86 @@ package org.android.go.sopt.presentation.login import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import org.android.go.sopt.data.entity.remote.request.RequestPostSignInDto import org.android.go.sopt.domain.model.User import org.android.go.sopt.domain.repository.AuthRepository -import org.android.go.sopt.util.UiState -import org.android.go.sopt.util.UiState.Failure -import org.android.go.sopt.util.UiState.Success +import org.android.go.sopt.util.state.RemoteUiState +import org.android.go.sopt.util.state.RemoteUiState.Error +import org.android.go.sopt.util.state.RemoteUiState.Failure +import org.android.go.sopt.util.state.RemoteUiState.Success +import retrofit2.HttpException +import timber.log.Timber import javax.inject.Inject @HiltViewModel class LoginViewModel @Inject constructor( private val authRepository: AuthRepository, ) : ViewModel() { - var signedUpUser = User() - - private val _loginState = MutableLiveData() - val loginState: LiveData + private val _loginState = MutableLiveData() + val loginState: LiveData get() = _loginState - val id = MutableLiveData("") - val pwd = MutableLiveData("") + val _id = MutableLiveData("") + private val id: String + get() = _id.value?.trim() ?: "" + + val _pwd = MutableLiveData("") + private val pwd: String + get() = _pwd.value?.trim() ?: "" init { setupAutoLogin() } private fun setupAutoLogin() { - if (authRepository.getAutoLogin() && authRepository.getSignedUpUser() != null) _loginState.value = Success - } - - fun setSavedUser(savedUser: User) { - this.signedUpUser = savedUser + if (authRepository.getAutoLogin() && authRepository.getSignedUpUser() != null) { + _loginState.value = RemoteUiState.Success + } } - private fun isValidInput() = - !id.value.isNullOrBlank() && id.value == signedUpUser.id && !pwd.value.isNullOrBlank() && pwd.value == signedUpUser.pwd + private fun isValidInput() = id.isNotBlank() && pwd.isNotBlank() fun login() { if (!isValidInput()) { _loginState.value = Failure(null) return } - authRepository.setAutoLogin(true) - _loginState.value = Success + + val requestPostSignInDto = RequestPostSignInDto( + id = id, + password = pwd, + ) + viewModelScope.launch { + authRepository.postSignin(requestPostSignInDto) + .onSuccess { response -> + authRepository.setSignedUpUser( + User( + id = requestPostSignInDto.id, + pwd = requestPostSignInDto.password, + name = requireNotNull(response).name, + specialty = response.skill, + ), + ) + authRepository.setAutoLogin(true) + _loginState.value = Success + Timber.d("POST SIGNIN SUCCESS : $response") + } + .onFailure { t -> + if (t is HttpException) { + when (t.code()) { + CODE_INVALID_INPUT -> _loginState.value = Failure(CODE_INVALID_INPUT) + else -> _loginState.value = Error + } + Timber.e("POST SIGNIN FAIL ${t.code()} : ${t.message()}") + } + } + } + } + + companion object { + const val CODE_INVALID_INPUT = 400 } } diff --git a/app/src/main/java/org/android/go/sopt/presentation/main/MainActivity.kt b/app/src/main/java/org/android/go/sopt/presentation/main/MainActivity.kt index 7e79d74..27d1c6a 100644 --- a/app/src/main/java/org/android/go/sopt/presentation/main/MainActivity.kt +++ b/app/src/main/java/org/android/go/sopt/presentation/main/MainActivity.kt @@ -7,6 +7,7 @@ import androidx.fragment.app.replace import dagger.hilt.android.AndroidEntryPoint import org.android.go.sopt.R import org.android.go.sopt.databinding.ActivityMainBinding +import org.android.go.sopt.presentation.main.follower.FollowerFragment import org.android.go.sopt.presentation.main.gallery.GalleryFragment import org.android.go.sopt.presentation.main.home.HomeFragment import org.android.go.sopt.presentation.main.profile.ProfileFragment @@ -30,6 +31,7 @@ class MainActivity : BindingActivity(R.layout.activity_main R.id.menu_home -> navigateTo() R.id.menu_gallery -> navigateTo() R.id.menu_search -> navigateTo() + R.id.menu_follower -> navigateTo() R.id.menu_profile -> navigateTo() } true diff --git a/app/src/main/java/org/android/go/sopt/presentation/main/follower/FollowerAdapter.kt b/app/src/main/java/org/android/go/sopt/presentation/main/follower/FollowerAdapter.kt new file mode 100644 index 0000000..3c04077 --- /dev/null +++ b/app/src/main/java/org/android/go/sopt/presentation/main/follower/FollowerAdapter.kt @@ -0,0 +1,77 @@ +package org.android.go.sopt.presentation.main.follower + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import org.android.go.sopt.R +import org.android.go.sopt.databinding.HeaderFollowerBinding +import org.android.go.sopt.databinding.ItemFollowerBinding +import org.android.go.sopt.domain.model.Follower +import org.android.go.sopt.util.DiffCallback + +class FollowerAdapter : ListAdapter(diffUtil) { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return when (viewType) { + VIEW_TYPE_HEADER -> FollowerHeaderViewHolder( + HeaderFollowerBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false, + ), + ) + + VIEW_TYPE_ITEM -> FollowerViewHolder( + ItemFollowerBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false, + ), + ) + + else -> throw ClassCastException( + parent.context.getString( + R.string.view_type_error_msg, + viewType, + ), + ) + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + if (holder is FollowerViewHolder) holder.setFollower(getItem(position - HEADER_COUNT)) + } + + override fun getItemCount(): Int { + return super.getItemCount() + HEADER_COUNT + } + + override fun getItemViewType(position: Int): Int { + return when (position) { + 0 -> VIEW_TYPE_HEADER + else -> VIEW_TYPE_ITEM + } + } + + class FollowerHeaderViewHolder(private val binding: HeaderFollowerBinding) : + RecyclerView.ViewHolder(binding.root) + + class FollowerViewHolder(private val binding: ItemFollowerBinding) : + RecyclerView.ViewHolder(binding.root) { + fun setFollower(follower: Follower) { + binding.data = follower + } + } + + companion object { + private val diffUtil = DiffCallback( + onItemsTheSame = { old, new -> old.id == new.id }, + onContentsTheSame = { old, new -> old == new }, + ) + + private const val HEADER_COUNT = 1 + + const val VIEW_TYPE_HEADER = 0 + const val VIEW_TYPE_ITEM = 1 + } +} diff --git a/app/src/main/java/org/android/go/sopt/presentation/main/follower/FollowerFragment.kt b/app/src/main/java/org/android/go/sopt/presentation/main/follower/FollowerFragment.kt new file mode 100644 index 0000000..d63d7c6 --- /dev/null +++ b/app/src/main/java/org/android/go/sopt/presentation/main/follower/FollowerFragment.kt @@ -0,0 +1,70 @@ +package org.android.go.sopt.presentation.main.follower + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup +import dagger.hilt.android.AndroidEntryPoint +import org.android.go.sopt.R +import org.android.go.sopt.databinding.FragmentFollowerBinding +import org.android.go.sopt.presentation.main.follower.FollowerAdapter.Companion.VIEW_TYPE_HEADER +import org.android.go.sopt.util.binding.BindingFragment +import org.android.go.sopt.util.extension.showSnackbar +import org.android.go.sopt.util.state.RemoteUiState.Error +import org.android.go.sopt.util.state.RemoteUiState.Failure +import org.android.go.sopt.util.state.RemoteUiState.Success + +@AndroidEntryPoint +class FollowerFragment : BindingFragment(R.layout.fragment_follower) { + private val viewModel by viewModels() + + private var followerAdapter: FollowerAdapter? = null + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.vm = viewModel + + initFollowerAdapter() + initRecyclerViewLayoutManager() + setupGetFollowerListState() + } + + private fun initFollowerAdapter() { + followerAdapter = FollowerAdapter() + binding.rvFollower.adapter = followerAdapter + } + + private fun initRecyclerViewLayoutManager() { + val layoutManager = GridLayoutManager(activity, 2) + layoutManager.spanSizeLookup = object : SpanSizeLookup() { + override fun getSpanSize(position: Int): Int { + return when (followerAdapter?.getItemViewType(position)) { + VIEW_TYPE_HEADER -> 2 + else -> 1 + } + } + } + binding.rvFollower.layoutManager = layoutManager + } + + private fun setupGetFollowerListState() { + viewModel.getFollowerListState.observe(viewLifecycleOwner) { state -> + when (state) { + is Success -> followerAdapter?.submitList(viewModel.followerList) + is Failure -> requireContext().showSnackbar(binding.root, getString(R.string.follower_get_follower_list_null_msg)) + is Error -> requireContext().showSnackbar(binding.root, getString(R.string.unknown_error_msg)) + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + followerAdapter = null + } + + companion object { + @JvmStatic + fun newInstance() = FollowerFragment() + } +} diff --git a/app/src/main/java/org/android/go/sopt/presentation/main/follower/FollowerViewModel.kt b/app/src/main/java/org/android/go/sopt/presentation/main/follower/FollowerViewModel.kt new file mode 100644 index 0000000..2a36681 --- /dev/null +++ b/app/src/main/java/org/android/go/sopt/presentation/main/follower/FollowerViewModel.kt @@ -0,0 +1,57 @@ +package org.android.go.sopt.presentation.main.follower + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import org.android.go.sopt.domain.model.Follower +import org.android.go.sopt.domain.repository.FollowerRepository +import org.android.go.sopt.util.state.RemoteUiState +import org.android.go.sopt.util.state.RemoteUiState.Error +import org.android.go.sopt.util.state.RemoteUiState.Failure +import org.android.go.sopt.util.state.RemoteUiState.Success +import retrofit2.HttpException +import timber.log.Timber +import javax.inject.Inject + +@HiltViewModel +class FollowerViewModel @Inject constructor( + private val followerRepository: FollowerRepository, +) : ViewModel() { + private val _followerList = MutableLiveData>() + val followerList: List? + get() = _followerList.value + + private val _getFollowerListState = MutableLiveData() + val getFollowerListState: LiveData + get() = _getFollowerListState + + init { + getFollowerList() + } + + private fun getFollowerList() { + Timber.d("get follower list 시작") + viewModelScope.launch { + followerRepository.getFollowerList(1) + .onSuccess { response -> + if (response.isEmpty()) { + _getFollowerListState.value = Failure(null) + return@onSuccess + } + + _followerList.value = response + _getFollowerListState.value = Success + Timber.d("GET FOLLOWER LIST SUCCESS : $response") + } + .onFailure { t -> + if (t is HttpException) { + _getFollowerListState.value = Error + Timber.e("GET FOLLOWER LIST FAIL : ${t.message}") + } + } + } + } +} diff --git a/app/src/main/java/org/android/go/sopt/presentation/main/gallery/GalleryFragment.kt b/app/src/main/java/org/android/go/sopt/presentation/main/gallery/GalleryFragment.kt index cebfbb3..70936b4 100644 --- a/app/src/main/java/org/android/go/sopt/presentation/main/gallery/GalleryFragment.kt +++ b/app/src/main/java/org/android/go/sopt/presentation/main/gallery/GalleryFragment.kt @@ -6,9 +6,31 @@ import org.android.go.sopt.R import org.android.go.sopt.databinding.FragmentGalleryBinding import org.android.go.sopt.util.binding.BindingFragment + class GalleryFragment : BindingFragment(R.layout.fragment_gallery) { + private var imageAdapter: ImageAdapter? = null + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + + initImageAdapter() + } + + private fun initImageAdapter() { + imageAdapter = ImageAdapter() + binding.vpGallery.adapter = imageAdapter + imageAdapter?.submitList( + listOf( + R.drawable.img_main_profile, + R.drawable.img_main_profile, + R.drawable.img_main_profile, + ), + ) + } + + override fun onDestroyView() { + super.onDestroyView() + imageAdapter = null } companion object { diff --git a/app/src/main/java/org/android/go/sopt/presentation/main/gallery/ImageAdapter.kt b/app/src/main/java/org/android/go/sopt/presentation/main/gallery/ImageAdapter.kt new file mode 100644 index 0000000..76d43b1 --- /dev/null +++ b/app/src/main/java/org/android/go/sopt/presentation/main/gallery/ImageAdapter.kt @@ -0,0 +1,40 @@ +package org.android.go.sopt.presentation.main.gallery + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.annotation.DrawableRes +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import org.android.go.sopt.databinding.ItemGalleryImageBinding +import org.android.go.sopt.util.DiffCallback + +class ImageAdapter : ListAdapter(diffUtil) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return ImageViewHolder( + ItemGalleryImageBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false, + ), + ) + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + if (holder is ImageViewHolder) holder.setImage(getItem(position)) + } + + class ImageViewHolder(private val binding: ItemGalleryImageBinding) : + RecyclerView.ViewHolder(binding.root) { + fun setImage(@DrawableRes index: Int) { + // TODO : 이미지 연결 + } + } + + companion object { + private val diffUtil = DiffCallback( + onItemsTheSame = { old, new -> old == new }, + onContentsTheSame = { old, new -> old == new }, + ) + } +} diff --git a/app/src/main/java/org/android/go/sopt/presentation/main/home/HomeFragment.kt b/app/src/main/java/org/android/go/sopt/presentation/main/home/HomeFragment.kt index d521ddb..03d50f4 100644 --- a/app/src/main/java/org/android/go/sopt/presentation/main/home/HomeFragment.kt +++ b/app/src/main/java/org/android/go/sopt/presentation/main/home/HomeFragment.kt @@ -7,10 +7,10 @@ import androidx.recyclerview.widget.ConcatAdapter import dagger.hilt.android.AndroidEntryPoint import org.android.go.sopt.R import org.android.go.sopt.databinding.FragmentHomeBinding -import org.android.go.sopt.util.UiState.Failure -import org.android.go.sopt.util.UiState.Success import org.android.go.sopt.util.binding.BindingFragment import org.android.go.sopt.util.extension.showSnackbar +import org.android.go.sopt.util.state.LocalUiState.Failure +import org.android.go.sopt.util.state.LocalUiState.Success @AndroidEntryPoint class HomeFragment : BindingFragment(R.layout.fragment_home) { diff --git a/app/src/main/java/org/android/go/sopt/presentation/main/home/HomeViewModel.kt b/app/src/main/java/org/android/go/sopt/presentation/main/home/HomeViewModel.kt index 20f2dbc..b841ff1 100644 --- a/app/src/main/java/org/android/go/sopt/presentation/main/home/HomeViewModel.kt +++ b/app/src/main/java/org/android/go/sopt/presentation/main/home/HomeViewModel.kt @@ -8,9 +8,9 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import org.android.go.sopt.domain.model.Repo import org.android.go.sopt.domain.repository.RepoRepository -import org.android.go.sopt.util.UiState -import org.android.go.sopt.util.UiState.Failure -import org.android.go.sopt.util.UiState.Success +import org.android.go.sopt.util.state.LocalUiState +import org.android.go.sopt.util.state.LocalUiState.Failure +import org.android.go.sopt.util.state.LocalUiState.Success import timber.log.Timber import javax.inject.Inject @@ -22,8 +22,8 @@ class HomeViewModel @Inject constructor( val repoList: LiveData> get() = _repoList - private val _getRepoListState = MutableLiveData() - val getRepoListState: LiveData + private val _getRepoListState = MutableLiveData() + val getRepoListState: LiveData get() = _getRepoListState init { diff --git a/app/src/main/java/org/android/go/sopt/presentation/signup/SignupActivity.kt b/app/src/main/java/org/android/go/sopt/presentation/signup/SignupActivity.kt index 6630a2d..c1efe8f 100644 --- a/app/src/main/java/org/android/go/sopt/presentation/signup/SignupActivity.kt +++ b/app/src/main/java/org/android/go/sopt/presentation/signup/SignupActivity.kt @@ -2,19 +2,22 @@ package org.android.go.sopt.presentation.signup import android.app.Activity import android.content.Intent -import android.content.Intent.EXTRA_USER import android.os.Bundle import androidx.activity.viewModels import dagger.hilt.android.AndroidEntryPoint import org.android.go.sopt.R import org.android.go.sopt.databinding.ActivitySignupBinding import org.android.go.sopt.presentation.login.LoginActivity +import org.android.go.sopt.presentation.signup.SignupViewModel.Companion.CODE_DUPLICATED_INFO import org.android.go.sopt.presentation.signup.SignupViewModel.Companion.CODE_INVALID_ID +import org.android.go.sopt.presentation.signup.SignupViewModel.Companion.CODE_INVALID_INPUT +import org.android.go.sopt.presentation.signup.SignupViewModel.Companion.CODE_INVALID_NAME import org.android.go.sopt.presentation.signup.SignupViewModel.Companion.CODE_INVALID_PWD -import org.android.go.sopt.util.UiState.Failure -import org.android.go.sopt.util.UiState.Success import org.android.go.sopt.util.binding.BindingActivity import org.android.go.sopt.util.extension.showSnackbar +import org.android.go.sopt.util.state.RemoteUiState.Error +import org.android.go.sopt.util.state.RemoteUiState.Failure +import org.android.go.sopt.util.state.RemoteUiState.Success @AndroidEntryPoint class SignupActivity : BindingActivity(R.layout.activity_signup) { @@ -42,15 +45,31 @@ class SignupActivity : BindingActivity(R.layout.activity_ binding.root, getString(R.string.signup_invalid_pwd_msg), ) + + CODE_INVALID_NAME -> showSnackbar( + binding.root, + getString(R.string.signup_invalid_name_msg), + ) + + CODE_INVALID_INPUT -> showSnackbar( + binding.root, + getString(R.string.wrong_input_msg), + ) + + CODE_DUPLICATED_INFO -> showSnackbar( + binding.root, + getString(R.string.signup_duplicated_info_msg), + ) } } + + is Error -> showSnackbar(binding.root, getString(R.string.unknown_error_msg)) } } } private fun navigateToLoginScreen() { Intent(this, LoginActivity::class.java).apply { - this.putExtra(EXTRA_USER, viewModel.getUser()) setResult(Activity.RESULT_OK, this) if (!isFinishing) finish() } diff --git a/app/src/main/java/org/android/go/sopt/presentation/signup/SignupViewModel.kt b/app/src/main/java/org/android/go/sopt/presentation/signup/SignupViewModel.kt index 31b8735..9bbb909 100644 --- a/app/src/main/java/org/android/go/sopt/presentation/signup/SignupViewModel.kt +++ b/app/src/main/java/org/android/go/sopt/presentation/signup/SignupViewModel.kt @@ -3,57 +3,89 @@ package org.android.go.sopt.presentation.signup import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import org.android.go.sopt.domain.model.User +import kotlinx.coroutines.launch +import org.android.go.sopt.data.entity.remote.request.RequestPostSignUpDto import org.android.go.sopt.domain.repository.AuthRepository -import org.android.go.sopt.util.UiState -import org.android.go.sopt.util.UiState.Failure -import org.android.go.sopt.util.UiState.Success -import org.android.go.sopt.util.safeValueOf -import org.android.go.sopt.util.type.MBTI.NONE +import org.android.go.sopt.util.state.RemoteUiState +import org.android.go.sopt.util.state.RemoteUiState.Error +import org.android.go.sopt.util.state.RemoteUiState.Failure +import org.android.go.sopt.util.state.RemoteUiState.Success +import retrofit2.HttpException +import timber.log.Timber import javax.inject.Inject @HiltViewModel class SignupViewModel @Inject constructor( private val authRepository: AuthRepository, ) : ViewModel() { - private val _signupState = MutableLiveData() - val signupState: LiveData + private val _signupState = MutableLiveData() + val signupState: LiveData get() = _signupState - val id = MutableLiveData("") - val pwd = MutableLiveData("") - val name = MutableLiveData("") - val specialty = MutableLiveData("") - val mbti = MutableLiveData("") + val _id = MutableLiveData("") + private val id: String + get() = _id.value?.trim() ?: "" - private fun isValidId(id: String?) = - !id.isNullOrBlank() && id.length in MIN_ID_LENGTH..MAX_ID_LENGTH + val _pwd = MutableLiveData("") + private val pwd: String + get() = _pwd.value?.trim() ?: "" - private fun isValidPwd(pwd: String?) = - !pwd.isNullOrBlank() && pwd.length in MIN_PWD_LENGTH..MAX_PWD_LENGTH + val _name = MutableLiveData("") + private val name: String + get() = _name.value?.trim() ?: "" + + val _specialty = MutableLiveData("") + private val specialty: String + get() = _specialty.value?.trim() ?: "" + + private fun isValidId() = id.isNotBlank() && id.length in MIN_ID_LENGTH..MAX_ID_LENGTH + + private fun isValidPwd() = pwd.isNotBlank() && pwd.length in MIN_PWD_LENGTH..MAX_PWD_LENGTH + + private fun isValidName() = name.isNotBlank() fun signup() { - if (!isValidId(id.value)) { + if (!isValidId()) { _signupState.value = Failure(CODE_INVALID_ID) return } - if (!isValidPwd(pwd.value)) { + + if (!isValidPwd()) { _signupState.value = Failure(CODE_INVALID_PWD) return } - authRepository.setSignedUpUser(getUser()) - _signupState.value = Success - } - fun getUser(): User { - return User( - requireNotNull(id.value).trim(), - requireNotNull(pwd.value).trim(), - name.value?.trim(), - specialty.value?.trim(), - safeValueOf(mbti.value?.trim()?.uppercase(), NONE), + if (!isValidName()) { + _signupState.value = Failure(CODE_INVALID_NAME) + return + } + + val requestPostSignUpDto = RequestPostSignUpDto( + id = id, + password = pwd, + name = name, + skill = specialty, ) + + viewModelScope.launch { + authRepository.postSignup(requestPostSignUpDto) + .onSuccess { response -> + _signupState.value = Success + Timber.d("POST SIGNUP SUCCESS : $response") + } + .onFailure { t -> + if (t is HttpException) { + when (t.code()) { + CODE_INVALID_INPUT -> _signupState.value = Failure(CODE_INVALID_INPUT) + CODE_DUPLICATED_INFO -> _signupState.value = Failure(CODE_DUPLICATED_INFO) + else -> _signupState.value = Error + } + Timber.e("POST SIGNUP FAIL ${t.code()} : ${t.message()}") + } + } + } } companion object { @@ -64,5 +96,8 @@ class SignupViewModel @Inject constructor( const val CODE_INVALID_ID = 100 const val CODE_INVALID_PWD = 101 + const val CODE_INVALID_NAME = 102 + const val CODE_INVALID_INPUT = 400 + const val CODE_DUPLICATED_INFO = 409 } } diff --git a/app/src/main/java/org/android/go/sopt/util/EnumUtil.kt b/app/src/main/java/org/android/go/sopt/util/EnumUtil.kt index e9fbffc..c2ada6b 100644 --- a/app/src/main/java/org/android/go/sopt/util/EnumUtil.kt +++ b/app/src/main/java/org/android/go/sopt/util/EnumUtil.kt @@ -1,6 +1,6 @@ package org.android.go.sopt.util -inline fun > safeValueOf(type: String?, default: T?): T? { +inline fun > safeValueOf(type: String?, default: T): T { return try { java.lang.Enum.valueOf(T::class.java, type ?: return default) } catch (e: IllegalArgumentException) { diff --git a/app/src/main/java/org/android/go/sopt/util/UiState.kt b/app/src/main/java/org/android/go/sopt/util/UiState.kt deleted file mode 100644 index 1713014..0000000 --- a/app/src/main/java/org/android/go/sopt/util/UiState.kt +++ /dev/null @@ -1,7 +0,0 @@ -package org.android.go.sopt.util - -sealed class UiState { - object Success : UiState() - data class Failure(val code: Int?) : UiState() -// object Error : UiState() -} diff --git a/app/src/main/java/org/android/go/sopt/util/binding/BindingAdapter.kt b/app/src/main/java/org/android/go/sopt/util/binding/BindingAdapter.kt index 99653e6..2b254fa 100644 --- a/app/src/main/java/org/android/go/sopt/util/binding/BindingAdapter.kt +++ b/app/src/main/java/org/android/go/sopt/util/binding/BindingAdapter.kt @@ -4,14 +4,28 @@ import android.widget.ImageView import androidx.databinding.BindingAdapter import coil.load import coil.transform.RoundedCornersTransformation +import org.android.go.sopt.R object BindingAdapter { @JvmStatic @BindingAdapter("setRoundedCornersImage") fun ImageView.setRoundedCornersImage(img: String?) { load(img) { - // TODO: placeholder & load error 이미지 추가 + placeholder(R.drawable.ic_loading) + error(R.drawable.ic_image_not_supported) + fallback(R.drawable.ic_image_not_supported) transformations(RoundedCornersTransformation(50f)) } } + + @JvmStatic + @BindingAdapter("setCircleImage") + fun ImageView.setCircleImage(img: String?) { + load(img) { + placeholder(R.drawable.ic_loading) + error(R.drawable.ic_image_not_supported) + fallback(R.drawable.ic_image_not_supported) + transformations(RoundedCornersTransformation(1000f)) + } + } } diff --git a/app/src/main/java/org/android/go/sopt/util/state/LocalUiState.kt b/app/src/main/java/org/android/go/sopt/util/state/LocalUiState.kt new file mode 100644 index 0000000..0e362ab --- /dev/null +++ b/app/src/main/java/org/android/go/sopt/util/state/LocalUiState.kt @@ -0,0 +1,6 @@ +package org.android.go.sopt.util.state + +sealed class LocalUiState { + object Success : LocalUiState() + data class Failure(val code: Int?) : LocalUiState() +} diff --git a/app/src/main/java/org/android/go/sopt/util/state/RemoteUiState.kt b/app/src/main/java/org/android/go/sopt/util/state/RemoteUiState.kt new file mode 100644 index 0000000..f670fcd --- /dev/null +++ b/app/src/main/java/org/android/go/sopt/util/state/RemoteUiState.kt @@ -0,0 +1,7 @@ +package org.android.go.sopt.util.state + +sealed class RemoteUiState { + object Success : RemoteUiState() + data class Failure(val code: Int?) : RemoteUiState() + object Error : RemoteUiState() +} diff --git a/app/src/main/java/org/android/go/sopt/util/type/BaseUrlType.kt b/app/src/main/java/org/android/go/sopt/util/type/BaseUrlType.kt new file mode 100644 index 0000000..a03a46c --- /dev/null +++ b/app/src/main/java/org/android/go/sopt/util/type/BaseUrlType.kt @@ -0,0 +1,5 @@ +package org.android.go.sopt.util.type + +enum class BaseUrlType { + SOPT, REQRES +} diff --git a/app/src/main/java/org/android/go/sopt/util/type/MBTI.kt b/app/src/main/java/org/android/go/sopt/util/type/MBTI.kt deleted file mode 100644 index 27b175d..0000000 --- a/app/src/main/java/org/android/go/sopt/util/type/MBTI.kt +++ /dev/null @@ -1,5 +0,0 @@ -package org.android.go.sopt.util.type - -enum class MBTI { - NONE, ISTJ, ISFJ, INFJ, INTJ, ISTP, ISFP, INFP, INTP, ESTP, ESFP, ENFP, ENTP, ESTJ, ESFJ, ENFJ, ENTJ -} diff --git a/app/src/main/res/drawable/ic_image_not_supported.xml b/app/src/main/res/drawable/ic_image_not_supported.xml new file mode 100644 index 0000000..d5a32e1 --- /dev/null +++ b/app/src/main/res/drawable/ic_image_not_supported.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_loading.xml b/app/src/main/res/drawable/ic_loading.xml new file mode 100644 index 0000000..fd59447 --- /dev/null +++ b/app/src/main/res/drawable/ic_loading.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_main_follower.xml b/app/src/main/res/drawable/ic_main_follower.xml new file mode 100644 index 0000000..a3ccd71 --- /dev/null +++ b/app/src/main/res/drawable/ic_main_follower.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index e722c17..7fc435a 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -50,7 +50,7 @@ android:imeOptions="actionNext" android:inputType="text" android:paddingVertical="16dp" - android:text="@={vm.id}" + android:text="@={vm._id}" android:textAppearance="?textAppearanceBodyLarge" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -78,7 +78,7 @@ android:imeOptions="actionDone" android:inputType="textPassword" android:paddingVertical="16dp" - android:text="@={vm.pwd}" + android:text="@={vm._pwd}" android:textAppearance="?textAppearanceBodyLarge" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/layout/activity_signup.xml b/app/src/main/res/layout/activity_signup.xml index 670e2d3..538261e 100644 --- a/app/src/main/res/layout/activity_signup.xml +++ b/app/src/main/res/layout/activity_signup.xml @@ -38,7 +38,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/login_id_label" - android:textAppearance="?attr/textAppearanceHeadlineSmall" + android:textAppearance="?textAppearanceHeadlineSmall" android:textColor="?colorOnBackground" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> @@ -53,7 +53,7 @@ android:inputType="text" android:maxLength="@{vm.MAX_ID_LENGTH}" android:paddingVertical="16dp" - android:text="@={vm.id}" + android:text="@={vm._id}" android:textAppearance="@style/TextAppearance.GoSopt.BodyLarge" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -80,7 +80,7 @@ android:inputType="textPassword" android:maxLength="@{vm.MAX_PWD_LENGTH}" android:paddingVertical="16dp" - android:text="@={vm.pwd}" + android:text="@={vm._pwd}" android:textAppearance="@style/TextAppearance.GoSopt.BodyLarge" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -106,7 +106,7 @@ android:imeOptions="actionNext" android:inputType="text|textPersonName" android:paddingVertical="16dp" - android:text="@={vm.name}" + android:text="@={vm._name}" android:textAppearance="@style/TextAppearance.GoSopt.BodyLarge" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -129,40 +129,14 @@ android:layout_height="wrap_content" android:layout_marginTop="2dp" android:hint="@string/signup_specialty_hint" - android:imeOptions="actionNext" + android:imeOptions="actionDone" android:inputType="text" android:paddingVertical="16dp" - android:text="@={vm.specialty}" + android:text="@={vm._specialty}" android:textAppearance="@style/TextAppearance.GoSopt.BodyLarge" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/tv_signup_specialty_label" /> - - - - diff --git a/app/src/main/res/layout/fragment_follower.xml b/app/src/main/res/layout/fragment_follower.xml new file mode 100644 index 0000000..c0cb34c --- /dev/null +++ b/app/src/main/res/layout/fragment_follower.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_gallery.xml b/app/src/main/res/layout/fragment_gallery.xml index 6a50998..fb913b9 100644 --- a/app/src/main/res/layout/fragment_gallery.xml +++ b/app/src/main/res/layout/fragment_gallery.xml @@ -13,13 +13,12 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - - -