diff --git a/.github/workflows/pr_checker.yml b/.github/workflows/pr_checker.yml index 6b9f85d0..ffe16277 100644 --- a/.github/workflows/pr_checker.yml +++ b/.github/workflows/pr_checker.yml @@ -43,8 +43,11 @@ jobs: - name: Add Local Properties env: AUTH_BASE_URL: ${{ secrets.AUTH_BASE_URL }} + KAKAO_NATIVE_KEY: ${{ secrets.KAKAO_NATIVE_KEY }} run: | echo auth.base.url=\"$AUTH_BASE_URL\" >> ./local.properties + echo kakao.native.key=\"$KAKAO_NATIVE_KEY\" >> ./local.properties + echo kakaoNativeKey=$KAKAO_NATIVE_KEY >> ./local.properties # - name: Access Firebase Service # run: echo '${{ secrets.GOOGLE_SERVICES_JSON }}' > ./app/google-services.json diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d3bd1606..47cddbf0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -26,6 +26,13 @@ android { "AUTH_BASE_URL", gradleLocalProperties(rootDir).getProperty("auth.base.url") ) + buildConfigField( + "String", + "KAKAO_NATIVE_KEY", + gradleLocalProperties(rootDir).getProperty("kakao.native.key") + ) + + manifestPlaceholders["KAKAO_NATIVE_KEY"] = gradleLocalProperties(rootDir).getProperty("kakaoNativeKey") testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } @@ -84,6 +91,8 @@ dependencies { implementation(workManager) implementation(hiltWorkManager) implementation(exif) + implementation(dataStore) + implementation(dataStoreCore) } TestDependencies.run { @@ -112,6 +121,7 @@ dependencies { implementation(balloon) implementation(lottie) implementation(circleImageView) + implementation(kakaoLogin) debugImplementation(flipper) debugImplementation(flipperNetwork) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c182e214..db8c2188 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -51,6 +51,25 @@ android:exported="false" android:screenOrientation="portrait" /> + + + + + + + + + + + + diff --git a/app/src/main/java/com/android/go/sopt/winey/WineyApplication.kt b/app/src/main/java/com/android/go/sopt/winey/WineyApplication.kt index 3d63fb04..3d26f949 100644 --- a/app/src/main/java/com/android/go/sopt/winey/WineyApplication.kt +++ b/app/src/main/java/com/android/go/sopt/winey/WineyApplication.kt @@ -1,6 +1,8 @@ package com.android.go.sopt.winey import android.app.Application +import com.android.go.sopt.winey.BuildConfig.KAKAO_NATIVE_KEY +import com.kakao.sdk.common.KakaoSdk import dagger.hilt.android.HiltAndroidApp import timber.log.Timber @@ -9,5 +11,6 @@ class WineyApplication : Application() { override fun onCreate() { super.onCreate() if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) + KakaoSdk.init(this, KAKAO_NATIVE_KEY) } } diff --git a/app/src/main/java/com/android/go/sopt/winey/data/interceptor/AuthInterceptor.kt b/app/src/main/java/com/android/go/sopt/winey/data/interceptor/AuthInterceptor.kt index f7450e0f..4ff7cea0 100644 --- a/app/src/main/java/com/android/go/sopt/winey/data/interceptor/AuthInterceptor.kt +++ b/app/src/main/java/com/android/go/sopt/winey/data/interceptor/AuthInterceptor.kt @@ -1,26 +1,89 @@ package com.android.go.sopt.winey.data.interceptor import android.content.Context +import com.android.go.sopt.winey.BuildConfig.AUTH_BASE_URL +import com.android.go.sopt.winey.data.model.remote.response.ResponseReIssueTokenDto +import com.android.go.sopt.winey.data.model.remote.response.base.BaseResponse +import com.android.go.sopt.winey.domain.repository.DataStoreRepository import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.Json import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response +import timber.log.Timber import javax.inject.Inject class AuthInterceptor @Inject constructor( - @ApplicationContext context: Context + @ApplicationContext context: Context, + private val json: Json, + private val dataStoreRepository: DataStoreRepository ) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val originalRequest = chain.request() - val headerRequest = originalRequest.newBuilder() - .addHeader(HEADER_TOKEN, USER_ID) + val headerRequest = originalRequest.newAuthBuilder() .build() - return chain.proceed(headerRequest) + val response = chain.proceed(headerRequest) + + when (response.code) { + CODE_TOKEN_EXPIRED -> { + try { + val refreshTokenRequest = originalRequest.newBuilder().post("".toRequestBody()) + .url("$AUTH_BASE_URL/auth/token") + .addHeader(REFRESH_TOKEN, runBlocking(Dispatchers.IO) { getRefreshToken() }) + .build() + val refreshTokenResponse = chain.proceed(refreshTokenRequest) + Timber.e("리프레시 토큰 : $refreshTokenResponse") + + if (refreshTokenResponse.isSuccessful) { + val responseToken = json.decodeFromString( + refreshTokenResponse.body?.string().toString() + ) as BaseResponse + + if (responseToken.data != null) { + saveAccessToken( + responseToken.data.accessToken, + responseToken.data.refreshToken + ) + } + refreshTokenResponse.close() + val newRequest = originalRequest.newAuthBuilder().build() + return chain.proceed(newRequest) + } + saveAccessToken("", "") + } catch (t: Throwable) { + Timber.e(t) + saveAccessToken("", "") + } + } + } + return response } + private fun Request.newAuthBuilder() = + this.newBuilder().addHeader(HEADER_TOKEN, runBlocking(Dispatchers.IO) { getAccessToken() }) + + private suspend fun getAccessToken(): String { + return dataStoreRepository.getAccessToken().first() ?: "" + } + + private suspend fun getRefreshToken(): String { + return dataStoreRepository.getAccessToken().first() ?: "" + } + + private fun saveAccessToken(accessToken: String, refreshToken: String) = + runBlocking { + dataStoreRepository.saveAccessToken(accessToken, refreshToken) + } + companion object { private const val HEADER_TOKEN = "accessToken" - const val USER_ID = "1" + private const val CODE_TOKEN_EXPIRED = 401 + private const val REFRESH_TOKEN = "refreshToken" } } diff --git a/app/src/main/java/com/android/go/sopt/winey/data/model/remote/request/RequestLoginDto.kt b/app/src/main/java/com/android/go/sopt/winey/data/model/remote/request/RequestLoginDto.kt new file mode 100644 index 00000000..fb3da3f8 --- /dev/null +++ b/app/src/main/java/com/android/go/sopt/winey/data/model/remote/request/RequestLoginDto.kt @@ -0,0 +1,10 @@ +package com.android.go.sopt.winey.data.model.remote.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RequestLoginDto( + @SerialName("socialType") + val socialType: String +) diff --git a/app/src/main/java/com/android/go/sopt/winey/data/model/remote/response/ResponseLoginDto.kt b/app/src/main/java/com/android/go/sopt/winey/data/model/remote/response/ResponseLoginDto.kt new file mode 100644 index 00000000..804ca1ad --- /dev/null +++ b/app/src/main/java/com/android/go/sopt/winey/data/model/remote/response/ResponseLoginDto.kt @@ -0,0 +1,16 @@ +package com.android.go.sopt.winey.data.model.remote.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseLoginDto( + @SerialName("userId") + val userId: Int, + @SerialName("accessToken") + val accessToken: String, + @SerialName("refreshToken") + val refreshToken: String, + @SerialName("isRegistered") + val isRegistered: Boolean +) diff --git a/app/src/main/java/com/android/go/sopt/winey/data/model/remote/response/ResponseReIssueTokenDto.kt b/app/src/main/java/com/android/go/sopt/winey/data/model/remote/response/ResponseReIssueTokenDto.kt new file mode 100644 index 00000000..60a6fc49 --- /dev/null +++ b/app/src/main/java/com/android/go/sopt/winey/data/model/remote/response/ResponseReIssueTokenDto.kt @@ -0,0 +1,12 @@ +package com.android.go.sopt.winey.data.model.remote.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseReIssueTokenDto( + @SerialName("accessToken") + val accessToken: String, + @SerialName("refreshToken") + val refreshToken: String +) diff --git a/app/src/main/java/com/android/go/sopt/winey/data/repository/AuthRepositoryImpl.kt b/app/src/main/java/com/android/go/sopt/winey/data/repository/AuthRepositoryImpl.kt index 7bb59e9e..22d2f353 100644 --- a/app/src/main/java/com/android/go/sopt/winey/data/repository/AuthRepositoryImpl.kt +++ b/app/src/main/java/com/android/go/sopt/winey/data/repository/AuthRepositoryImpl.kt @@ -1,8 +1,11 @@ package com.android.go.sopt.winey.data.repository import com.android.go.sopt.winey.data.model.remote.request.RequestCreateGoalDto +import com.android.go.sopt.winey.data.model.remote.request.RequestLoginDto import com.android.go.sopt.winey.data.model.remote.request.RequestPostLikeDto +import com.android.go.sopt.winey.data.model.remote.response.ResponseLoginDto import com.android.go.sopt.winey.data.model.remote.response.ResponsePostWineyFeedDto +import com.android.go.sopt.winey.data.model.remote.response.ResponseReIssueTokenDto import com.android.go.sopt.winey.data.source.AuthDataSource import com.android.go.sopt.winey.domain.entity.Goal import com.android.go.sopt.winey.domain.entity.Like @@ -17,9 +20,9 @@ import javax.inject.Inject class AuthRepositoryImpl @Inject constructor( private val authDataSource: AuthDataSource ) : AuthRepository { - override suspend fun getUser(): Result = + override suspend fun getUser(): Result = runCatching { - authDataSource.getUser().data!!.toUser() + authDataSource.getUser().data?.toUser() } override suspend fun getWineyFeedList(page: Int): Result> = @@ -46,18 +49,34 @@ class AuthRepositoryImpl @Inject constructor( authDataSource.deleteFeed(feedId) } - override suspend fun postFeedLike(feedId: Int, requestPostLikeDto: RequestPostLikeDto): Result = + override suspend fun postFeedLike( + feedId: Int, + requestPostLikeDto: RequestPostLikeDto + ): Result = runCatching { authDataSource.postFeedLike(feedId, requestPostLikeDto).toLike() } - override suspend fun postCreateGoal(requestCreateGoalDto: RequestCreateGoalDto): Result = + override suspend fun postCreateGoal(requestCreateGoalDto: RequestCreateGoalDto): Result = runCatching { - authDataSource.postCreateGoal(requestCreateGoalDto).data!!.toGoal() + authDataSource.postCreateGoal(requestCreateGoalDto).data?.toGoal() } - override suspend fun getRecommendList(page: Int): Result> = + override suspend fun getRecommendList(page: Int): Result?> = runCatching { - authDataSource.getRecommendList(page).data!!.convertToRecommend() + authDataSource.getRecommendList(page).data?.convertToRecommend() + } + + override suspend fun postLogin( + socialAccessToken: String, + requestLoginDto: RequestLoginDto + ): Result = + runCatching { + authDataSource.postLogin(socialAccessToken, requestLoginDto).data + } + + override suspend fun postReIssueToken(refreshToken: String): Result = + runCatching { + authDataSource.postReIssueToken(refreshToken).data } } diff --git a/app/src/main/java/com/android/go/sopt/winey/data/repository/DataStoreRepositoryImpl.kt b/app/src/main/java/com/android/go/sopt/winey/data/repository/DataStoreRepositoryImpl.kt new file mode 100644 index 00000000..4a56bdfb --- /dev/null +++ b/app/src/main/java/com/android/go/sopt/winey/data/repository/DataStoreRepositoryImpl.kt @@ -0,0 +1,121 @@ +package com.android.go.sopt.winey.data.repository + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.emptyPreferences +import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey +import com.android.go.sopt.winey.domain.entity.User +import com.android.go.sopt.winey.domain.repository.DataStoreRepository +import com.google.gson.Gson +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map +import java.io.IOException +import javax.inject.Inject + +class DataStoreRepositoryImpl @Inject constructor( + val datastore: DataStore +) : DataStoreRepository { + + override suspend fun saveSocialToken(socialAccessToken: String, socialRefreshToken: String) { + datastore.edit { + it[SOCIAL_ACCESS_TOKEN] = socialAccessToken + it[SOCIAL_REFRESH_TOKEN] = socialRefreshToken + } + } + + override suspend fun getSocialAccessToken(): Flow { + return getStringValue(SOCIAL_ACCESS_TOKEN) + } + + override suspend fun saveAccessToken(accessToken: String, refreshToken: String) { + datastore.edit { + it[ACCESS_TOKEN] = accessToken + it[REFRESH_TOKEN] = refreshToken + } + } + + override suspend fun saveUserId(userId: Int) { + datastore.edit { + it[USER_ID] = userId + } + } + + override suspend fun getAccessToken(): Flow { + return getStringValue(ACCESS_TOKEN) + } + + override suspend fun getRefreshToken(): Flow { + return getStringValue(REFRESH_TOKEN) + } + + override suspend fun getStringValue(key: Preferences.Key): Flow { + return datastore.data + .catch { exception -> + if (exception is IOException) { + exception.printStackTrace() + emit(emptyPreferences()) + } else { + throw exception + } + } + .map { + it[key] + } + } + + override suspend fun getUserId(): Flow { + return datastore.data + .catch { exception -> + if (exception is IOException) { + exception.printStackTrace() + emit(emptyPreferences()) + } else { + throw exception + } + } + .map { + it[USER_ID] + } + } + + override suspend fun saveUserInfo(userInfo: User?) { + datastore.edit { + val json = Gson().toJson(userInfo) + it[USER_INFO] = json + } + } + + override suspend fun getUserInfo(): Flow { + return datastore.data + .catch { exception -> + if (exception is IOException) { + exception.printStackTrace() + emit(emptyPreferences()) + } else { + throw exception + } + } + .map { + val json = it[USER_INFO] + try { + Gson().fromJson(json, User::class.java) + } catch (e: Exception) { + User() + } + } + } + + companion object PreferencesKeys { + private val SOCIAL_ACCESS_TOKEN: Preferences.Key = + stringPreferencesKey("social_access_token") + private val SOCIAL_REFRESH_TOKEN: Preferences.Key = + stringPreferencesKey("social_refresh_token") + private val ACCESS_TOKEN: Preferences.Key = stringPreferencesKey("access_token") + private val REFRESH_TOKEN: Preferences.Key = stringPreferencesKey("refresh_token") + private val USER_ID: Preferences.Key = intPreferencesKey("user_id") + private val USER_INFO: Preferences.Key = stringPreferencesKey("user_info") + } +} diff --git a/app/src/main/java/com/android/go/sopt/winey/data/repository/KakaoLoginRepositoryImpl.kt b/app/src/main/java/com/android/go/sopt/winey/data/repository/KakaoLoginRepositoryImpl.kt new file mode 100644 index 00000000..3f4a99a7 --- /dev/null +++ b/app/src/main/java/com/android/go/sopt/winey/data/repository/KakaoLoginRepositoryImpl.kt @@ -0,0 +1,23 @@ +package com.android.go.sopt.winey.data.repository + +import android.content.Context +import com.android.go.sopt.winey.data.source.KakaoLoginDataSource +import com.android.go.sopt.winey.domain.repository.KakaoLoginRepository +import com.kakao.sdk.auth.model.OAuthToken +import javax.inject.Inject + +class KakaoLoginRepositoryImpl @Inject constructor( + private val kakaoLoginDataSource: KakaoLoginDataSource +) : KakaoLoginRepository { + override fun loginKakao(kakaoLoginCallBack: (OAuthToken?, Throwable?) -> Unit, context: Context) { + kakaoLoginDataSource.loginKakao(kakaoLoginCallBack, context) + } + + override fun logoutKakao(kakaoLogoutCallBack: (Throwable?) -> Unit) { + kakaoLoginDataSource.logoutKakao(kakaoLogoutCallBack) + } + + override fun deleteKakaoAccount(kakaoLogoutCallBack: (Throwable?) -> Unit) { + kakaoLoginDataSource.deleteKakaoAccount(kakaoLogoutCallBack) + } +} diff --git a/app/src/main/java/com/android/go/sopt/winey/data/service/AuthService.kt b/app/src/main/java/com/android/go/sopt/winey/data/service/AuthService.kt index 3f80455b..c64493e7 100644 --- a/app/src/main/java/com/android/go/sopt/winey/data/service/AuthService.kt +++ b/app/src/main/java/com/android/go/sopt/winey/data/service/AuthService.kt @@ -1,19 +1,23 @@ package com.android.go.sopt.winey.data.service import com.android.go.sopt.winey.data.model.remote.request.RequestCreateGoalDto +import com.android.go.sopt.winey.data.model.remote.request.RequestLoginDto import com.android.go.sopt.winey.data.model.remote.request.RequestPostLikeDto import com.android.go.sopt.winey.data.model.remote.response.ResponseCreateGoalDto import com.android.go.sopt.winey.data.model.remote.response.ResponseGetRecommendListDto import com.android.go.sopt.winey.data.model.remote.response.ResponseGetUserDto import com.android.go.sopt.winey.data.model.remote.response.ResponseGetWineyFeedListDto +import com.android.go.sopt.winey.data.model.remote.response.ResponseLoginDto import com.android.go.sopt.winey.data.model.remote.response.ResponsePostLikeDto import com.android.go.sopt.winey.data.model.remote.response.ResponsePostWineyFeedDto +import com.android.go.sopt.winey.data.model.remote.response.ResponseReIssueTokenDto import com.android.go.sopt.winey.data.model.remote.response.base.BaseResponse import okhttp3.MultipartBody import okhttp3.RequestBody import retrofit2.http.Body import retrofit2.http.DELETE import retrofit2.http.GET +import retrofit2.http.Header import retrofit2.http.Multipart import retrofit2.http.POST import retrofit2.http.Part @@ -23,7 +27,7 @@ import retrofit2.http.Query interface AuthService { @GET("user") - suspend fun getUser(): BaseResponse + suspend fun getUser(): BaseResponse @GET("feed") suspend fun getWineyFeedList( @@ -62,4 +66,15 @@ interface AuthService { suspend fun deleteFeed( @Path("feedId") feedId: Int ): BaseResponse + + @POST("auth") + suspend fun postLogin( + @Header("Authorization") socialAccessToken: String, + @Body requestLoginDto: RequestLoginDto + ): BaseResponse + + @POST("auth/token") + suspend fun postReIssueToken( + @Header("refreshToken") refreshToken: String + ): BaseResponse } diff --git a/app/src/main/java/com/android/go/sopt/winey/data/service/KakaoLoginService.kt b/app/src/main/java/com/android/go/sopt/winey/data/service/KakaoLoginService.kt new file mode 100644 index 00000000..cda7dc46 --- /dev/null +++ b/app/src/main/java/com/android/go/sopt/winey/data/service/KakaoLoginService.kt @@ -0,0 +1,10 @@ +package com.android.go.sopt.winey.data.service + +import android.content.Context +import com.kakao.sdk.auth.model.OAuthToken + +interface KakaoLoginService { + fun loginKakao(kakaoLoginCallBack: (OAuthToken?, Throwable?) -> Unit, context: Context) + fun logoutKakao(kakaoLogoutCallBack: (Throwable?) -> Unit) + fun deleteKakaoAccount(kakaoLogoutCallBack: (Throwable?) -> Unit) +} diff --git a/app/src/main/java/com/android/go/sopt/winey/data/source/AuthDataSource.kt b/app/src/main/java/com/android/go/sopt/winey/data/source/AuthDataSource.kt index c338870c..532fb32a 100644 --- a/app/src/main/java/com/android/go/sopt/winey/data/source/AuthDataSource.kt +++ b/app/src/main/java/com/android/go/sopt/winey/data/source/AuthDataSource.kt @@ -1,13 +1,16 @@ package com.android.go.sopt.winey.data.source import com.android.go.sopt.winey.data.model.remote.request.RequestCreateGoalDto +import com.android.go.sopt.winey.data.model.remote.request.RequestLoginDto import com.android.go.sopt.winey.data.model.remote.request.RequestPostLikeDto import com.android.go.sopt.winey.data.model.remote.response.ResponseCreateGoalDto import com.android.go.sopt.winey.data.model.remote.response.ResponseGetRecommendListDto import com.android.go.sopt.winey.data.model.remote.response.ResponseGetUserDto import com.android.go.sopt.winey.data.model.remote.response.ResponseGetWineyFeedListDto +import com.android.go.sopt.winey.data.model.remote.response.ResponseLoginDto import com.android.go.sopt.winey.data.model.remote.response.ResponsePostLikeDto import com.android.go.sopt.winey.data.model.remote.response.ResponsePostWineyFeedDto +import com.android.go.sopt.winey.data.model.remote.response.ResponseReIssueTokenDto import com.android.go.sopt.winey.data.model.remote.response.base.BaseResponse import com.android.go.sopt.winey.data.service.AuthService import okhttp3.MultipartBody @@ -17,7 +20,7 @@ import javax.inject.Inject class AuthDataSource @Inject constructor( private val authService: AuthService ) { - suspend fun getUser(): BaseResponse = authService.getUser() + suspend fun getUser(): BaseResponse = authService.getUser() suspend fun getWineyFeedList(page: Int): ResponseGetWineyFeedListDto = authService.getWineyFeedList(page) @@ -25,7 +28,10 @@ class AuthDataSource @Inject constructor( suspend fun getMyFeedList(page: Int): ResponseGetWineyFeedListDto = authService.getMyFeedList(page) - suspend fun postFeedLike(feedId: Int, requestPostLikeDto: RequestPostLikeDto): ResponsePostLikeDto = + suspend fun postFeedLike( + feedId: Int, + requestPostLikeDto: RequestPostLikeDto + ): ResponsePostLikeDto = authService.postFeedLike(feedId, requestPostLikeDto) suspend fun postWineyFeedList( @@ -39,6 +45,18 @@ class AuthDataSource @Inject constructor( suspend fun getRecommendList(page: Int): BaseResponse = authService.getRecommendList(page) + suspend fun deleteFeed(feedId: Int): BaseResponse = authService.deleteFeed(feedId) + + suspend fun postLogin( + socialAccessToken: String, + requestLoginDto: RequestLoginDto + ): BaseResponse = + authService.postLogin(socialAccessToken, requestLoginDto) + + suspend fun postReIssueToken( + refreshToken: String + ): BaseResponse = + authService.postReIssueToken(refreshToken) } diff --git a/app/src/main/java/com/android/go/sopt/winey/data/source/KakaoLoginDataSource.kt b/app/src/main/java/com/android/go/sopt/winey/data/source/KakaoLoginDataSource.kt new file mode 100644 index 00000000..ee959f6e --- /dev/null +++ b/app/src/main/java/com/android/go/sopt/winey/data/source/KakaoLoginDataSource.kt @@ -0,0 +1,19 @@ +package com.android.go.sopt.winey.data.source + +import android.content.Context +import com.android.go.sopt.winey.data.service.KakaoLoginService +import com.kakao.sdk.auth.model.OAuthToken +import javax.inject.Inject + +class KakaoLoginDataSource @Inject constructor( + private val kakaoLoginService: KakaoLoginService +) { + fun loginKakao(kakaoLoginCallBack: (OAuthToken?, Throwable?) -> Unit, context: Context) = + kakaoLoginService.loginKakao(kakaoLoginCallBack, context) + + fun logoutKakao(kakaoLogoutCallBack: (Throwable?) -> Unit) = + kakaoLoginService.logoutKakao(kakaoLogoutCallBack) + + fun deleteKakaoAccount(kakaoLogoutCallBack: (Throwable?) -> Unit) = + kakaoLoginService.deleteKakaoAccount(kakaoLogoutCallBack) +} diff --git a/app/src/main/java/com/android/go/sopt/winey/di/DataStoreModule.kt b/app/src/main/java/com/android/go/sopt/winey/di/DataStoreModule.kt new file mode 100644 index 00000000..62941fa9 --- /dev/null +++ b/app/src/main/java/com/android/go/sopt/winey/di/DataStoreModule.kt @@ -0,0 +1,22 @@ +package com.android.go.sopt.winey.di + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.preferencesDataStore +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +object DataStoreModule { + private val Context.dataStore: DataStore by preferencesDataStore(name = "winey_data_store") + + @Provides + fun provideDataStore(@ApplicationContext context: Context): DataStore { + return context.dataStore + } +} diff --git a/app/src/main/java/com/android/go/sopt/winey/di/KakaoModule.kt b/app/src/main/java/com/android/go/sopt/winey/di/KakaoModule.kt new file mode 100644 index 00000000..2061569a --- /dev/null +++ b/app/src/main/java/com/android/go/sopt/winey/di/KakaoModule.kt @@ -0,0 +1,16 @@ +package com.android.go.sopt.winey.di + +import com.kakao.sdk.user.UserApiClient +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object KakaoModule { + @Provides + @Singleton + fun provideKakaoApiClient(): UserApiClient = UserApiClient.instance +} diff --git a/app/src/main/java/com/android/go/sopt/winey/di/RepositoryModule.kt b/app/src/main/java/com/android/go/sopt/winey/di/RepositoryModule.kt index 83cfaadc..ebfde822 100644 --- a/app/src/main/java/com/android/go/sopt/winey/di/RepositoryModule.kt +++ b/app/src/main/java/com/android/go/sopt/winey/di/RepositoryModule.kt @@ -1,7 +1,11 @@ package com.android.go.sopt.winey.di import com.android.go.sopt.winey.data.repository.AuthRepositoryImpl +import com.android.go.sopt.winey.data.repository.DataStoreRepositoryImpl +import com.android.go.sopt.winey.data.repository.KakaoLoginRepositoryImpl import com.android.go.sopt.winey.domain.repository.AuthRepository +import com.android.go.sopt.winey.domain.repository.DataStoreRepository +import com.android.go.sopt.winey.domain.repository.KakaoLoginRepository import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -16,4 +20,16 @@ abstract class RepositoryModule { abstract fun bindsAuthRepository( authRepository: AuthRepositoryImpl ): AuthRepository + + @Singleton + @Binds + abstract fun bindsKakaoLoginRepository( + kakaoLoginRepository: KakaoLoginRepositoryImpl + ): KakaoLoginRepository + + @Singleton + @Binds + abstract fun bindsDataStoreRepository( + dataStoreRepository: DataStoreRepositoryImpl + ): DataStoreRepository } diff --git a/app/src/main/java/com/android/go/sopt/winey/di/ServiceModule.kt b/app/src/main/java/com/android/go/sopt/winey/di/ServiceModule.kt index e13a6d8e..9f36e3b1 100644 --- a/app/src/main/java/com/android/go/sopt/winey/di/ServiceModule.kt +++ b/app/src/main/java/com/android/go/sopt/winey/di/ServiceModule.kt @@ -1,6 +1,10 @@ package com.android.go.sopt.winey.di +import android.content.Context import com.android.go.sopt.winey.data.service.AuthService +import com.android.go.sopt.winey.data.service.KakaoLoginService +import com.kakao.sdk.auth.model.OAuthToken +import com.kakao.sdk.user.UserApiClient import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -11,8 +15,54 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) object ServiceModule { + private const val KAKAO_TALK_LOGIN = 0 + private const val KAKAO_ACCOUNT_LOGIN = 1 + @Provides @Singleton fun provideAuthService(retrofit: Retrofit): AuthService = retrofit.create(AuthService::class.java) + + @Provides + fun provideKakaoLoginService( + client: UserApiClient + ): KakaoLoginService { + return object : KakaoLoginService { + override fun loginKakao( + kakaoLoginCallBack: (OAuthToken?, Throwable?) -> Unit, + context: Context + ) { + val kakaoLoginState = + if (client.isKakaoTalkLoginAvailable(context)) { + KAKAO_TALK_LOGIN + } else { + KAKAO_ACCOUNT_LOGIN + } + + when (kakaoLoginState) { + KAKAO_TALK_LOGIN -> { + client.loginWithKakaoTalk( + context, + callback = kakaoLoginCallBack + ) + } + + KAKAO_ACCOUNT_LOGIN -> { + client.loginWithKakaoAccount( + context, + callback = kakaoLoginCallBack + ) + } + } + } + + override fun logoutKakao(kakaoLogoutCallBack: (Throwable?) -> Unit) { + client.logout(kakaoLogoutCallBack) + } + + override fun deleteKakaoAccount(kakaoLogoutCallBack: (Throwable?) -> Unit) { + client.unlink(kakaoLogoutCallBack) + } + } + } } diff --git a/app/src/main/java/com/android/go/sopt/winey/domain/entity/Login.kt b/app/src/main/java/com/android/go/sopt/winey/domain/entity/Login.kt new file mode 100644 index 00000000..2ddbe675 --- /dev/null +++ b/app/src/main/java/com/android/go/sopt/winey/domain/entity/Login.kt @@ -0,0 +1,8 @@ +package com.android.go.sopt.winey.domain.entity + +data class Login( + val userId: Int, + val accessToken: String, + val refreshToken: String, + val isRegistered: Boolean +) diff --git a/app/src/main/java/com/android/go/sopt/winey/domain/entity/ReIssueToken.kt b/app/src/main/java/com/android/go/sopt/winey/domain/entity/ReIssueToken.kt new file mode 100644 index 00000000..cf5e6588 --- /dev/null +++ b/app/src/main/java/com/android/go/sopt/winey/domain/entity/ReIssueToken.kt @@ -0,0 +1,6 @@ +package com.android.go.sopt.winey.domain.entity + +data class ReIssueToken( + val accessToken: String, + val refreshToken: String +) diff --git a/app/src/main/java/com/android/go/sopt/winey/domain/entity/User.kt b/app/src/main/java/com/android/go/sopt/winey/domain/entity/User.kt index 6d8ca287..421eb13f 100644 --- a/app/src/main/java/com/android/go/sopt/winey/domain/entity/User.kt +++ b/app/src/main/java/com/android/go/sopt/winey/domain/entity/User.kt @@ -1,13 +1,13 @@ package com.android.go.sopt.winey.domain.entity data class User( - val nickname: String, - val userLevel: String, - val duringGoalAmount: Long, - val duringGoalCount: Long, - val targetMoney: Int, - val targetDay: Int, - val dday: Int, - val isOver: Boolean, - val isAttained: Boolean + val nickname: String = "", + val userLevel: String = "", + val duringGoalAmount: Long = 0, + val duringGoalCount: Long = 0, + val targetMoney: Int = 0, + val targetDay: Int = 0, + val dday: Int = 0, + val isOver: Boolean = false, + val isAttained: Boolean = false ) diff --git a/app/src/main/java/com/android/go/sopt/winey/domain/repository/AuthRepository.kt b/app/src/main/java/com/android/go/sopt/winey/domain/repository/AuthRepository.kt index 13c4e00a..951abdf3 100644 --- a/app/src/main/java/com/android/go/sopt/winey/domain/repository/AuthRepository.kt +++ b/app/src/main/java/com/android/go/sopt/winey/domain/repository/AuthRepository.kt @@ -1,8 +1,11 @@ package com.android.go.sopt.winey.domain.repository import com.android.go.sopt.winey.data.model.remote.request.RequestCreateGoalDto +import com.android.go.sopt.winey.data.model.remote.request.RequestLoginDto import com.android.go.sopt.winey.data.model.remote.request.RequestPostLikeDto +import com.android.go.sopt.winey.data.model.remote.response.ResponseLoginDto import com.android.go.sopt.winey.data.model.remote.response.ResponsePostWineyFeedDto +import com.android.go.sopt.winey.data.model.remote.response.ResponseReIssueTokenDto import com.android.go.sopt.winey.domain.entity.Goal import com.android.go.sopt.winey.domain.entity.Like import com.android.go.sopt.winey.domain.entity.Recommend @@ -12,7 +15,7 @@ import okhttp3.MultipartBody import okhttp3.RequestBody interface AuthRepository { - suspend fun getUser(): Result + suspend fun getUser(): Result suspend fun getWineyFeedList(page: Int): Result> @@ -25,8 +28,15 @@ interface AuthRepository { requestMap: HashMap ): Result - suspend fun postCreateGoal(requestCreateGoalDto: RequestCreateGoalDto): Result + suspend fun postCreateGoal(requestCreateGoalDto: RequestCreateGoalDto): Result - suspend fun getRecommendList(page: Int): Result> + suspend fun getRecommendList(page: Int): Result?> suspend fun deleteFeed(feedId: Int): Result + + suspend fun postLogin( + socialAccessToken: String, + requestLoginDto: RequestLoginDto + ): Result + + suspend fun postReIssueToken(refreshToken: String): Result } diff --git a/app/src/main/java/com/android/go/sopt/winey/domain/repository/DataStoreRepository.kt b/app/src/main/java/com/android/go/sopt/winey/domain/repository/DataStoreRepository.kt new file mode 100644 index 00000000..77ddf2c6 --- /dev/null +++ b/app/src/main/java/com/android/go/sopt/winey/domain/repository/DataStoreRepository.kt @@ -0,0 +1,27 @@ +package com.android.go.sopt.winey.domain.repository + +import androidx.datastore.preferences.core.Preferences +import com.android.go.sopt.winey.domain.entity.User +import kotlinx.coroutines.flow.Flow + +interface DataStoreRepository { + suspend fun saveSocialToken(socialAccessToken: String, socialRefreshToken: String) + + suspend fun getSocialAccessToken(): Flow + + suspend fun saveAccessToken(accessToken: String = "", refreshToken: String = "") + + suspend fun saveUserId(userId: Int = 0) + + suspend fun getAccessToken(): Flow + + suspend fun getRefreshToken(): Flow + + suspend fun getStringValue(key: Preferences.Key): Flow + + suspend fun getUserId(): Flow + + suspend fun saveUserInfo(userInfo: User?) + + suspend fun getUserInfo(): Flow +} diff --git a/app/src/main/java/com/android/go/sopt/winey/domain/repository/KakaoLoginRepository.kt b/app/src/main/java/com/android/go/sopt/winey/domain/repository/KakaoLoginRepository.kt new file mode 100644 index 00000000..c5be5d72 --- /dev/null +++ b/app/src/main/java/com/android/go/sopt/winey/domain/repository/KakaoLoginRepository.kt @@ -0,0 +1,12 @@ +package com.android.go.sopt.winey.domain.repository + +import android.content.Context +import com.kakao.sdk.auth.model.OAuthToken + +interface KakaoLoginRepository { + fun loginKakao(kakaoLoginCallBack: (OAuthToken?, Throwable?) -> Unit, context: Context) + + fun logoutKakao(kakaoLogoutCallBack: (Throwable?) -> Unit) + + fun deleteKakaoAccount(kakaoLogoutCallBack: (Throwable?) -> Unit) +} diff --git a/app/src/main/java/com/android/go/sopt/winey/presentation/main/MainViewModel.kt b/app/src/main/java/com/android/go/sopt/winey/presentation/main/MainViewModel.kt index a247e440..39378ce3 100644 --- a/app/src/main/java/com/android/go/sopt/winey/presentation/main/MainViewModel.kt +++ b/app/src/main/java/com/android/go/sopt/winey/presentation/main/MainViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.android.go.sopt.winey.domain.entity.User import com.android.go.sopt.winey.domain.repository.AuthRepository +import com.android.go.sopt.winey.domain.repository.DataStoreRepository import com.android.go.sopt.winey.util.view.UiState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -16,10 +17,11 @@ import javax.inject.Inject @HiltViewModel class MainViewModel @Inject constructor( - private val authRepository: AuthRepository + private val authRepository: AuthRepository, + private val dataStoreRepository: DataStoreRepository ) : ViewModel() { - private val _getUserState = MutableStateFlow>(UiState.Loading) - val getUserState: StateFlow> get() = _getUserState.asStateFlow() + private val _getUserState = MutableStateFlow>(UiState.Loading) + val getUserState: StateFlow> = _getUserState.asStateFlow() init { getUser() @@ -31,6 +33,7 @@ class MainViewModel @Inject constructor( authRepository.getUser() .onSuccess { response -> + dataStoreRepository.saveUserInfo(response) _getUserState.value = UiState.Success(response) Timber.e("메인뷰모델 성공") } diff --git a/app/src/main/java/com/android/go/sopt/winey/presentation/main/feed/WineyFeedFragment.kt b/app/src/main/java/com/android/go/sopt/winey/presentation/main/feed/WineyFeedFragment.kt index bbe0cc3b..327d311f 100644 --- a/app/src/main/java/com/android/go/sopt/winey/presentation/main/feed/WineyFeedFragment.kt +++ b/app/src/main/java/com/android/go/sopt/winey/presentation/main/feed/WineyFeedFragment.kt @@ -12,10 +12,10 @@ import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.android.go.sopt.winey.R -import com.android.go.sopt.winey.data.interceptor.AuthInterceptor import com.android.go.sopt.winey.databinding.FragmentWineyFeedBinding import com.android.go.sopt.winey.domain.entity.User import com.android.go.sopt.winey.domain.entity.WineyFeed +import com.android.go.sopt.winey.domain.repository.DataStoreRepository import com.android.go.sopt.winey.presentation.main.MainViewModel import com.android.go.sopt.winey.presentation.main.feed.upload.UploadActivity import com.android.go.sopt.winey.util.binding.BindingFragment @@ -26,11 +26,14 @@ import com.android.go.sopt.winey.util.view.UiState import com.android.go.sopt.winey.util.view.setOnSingleClickListener import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import timber.log.Timber +import javax.inject.Inject @AndroidEntryPoint class WineyFeedFragment : BindingFragment(R.layout.fragment_winey_feed) { @@ -39,6 +42,10 @@ class WineyFeedFragment : BindingFragment(R.layout.fra private lateinit var wineyFeedDialogFragment: WineyFeedDialogFragment private lateinit var wineyFeedAdapter: WineyFeedAdapter private lateinit var wineyFeedHeaderAdapter: WineyFeedHeaderAdapter + + @Inject + lateinit var dataStoreRepository: DataStoreRepository + private var totalPage = Int.MAX_VALUE override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -70,7 +77,7 @@ class WineyFeedFragment : BindingFragment(R.layout.fra val menuDelete = popupMenu.menu.findItem(R.id.menu_delete) val menuReport = popupMenu.menu.findItem(R.id.menu_report) //TODO: 로그인 완료되면 리팩토링 - if (wineyFeed.userId == AuthInterceptor.USER_ID.toInt()) { + if (wineyFeed.userId == runBlocking { dataStoreRepository.getUserId().first() }) { menuReport.isVisible = false } else { menuDelete.isVisible = false @@ -142,7 +149,8 @@ class WineyFeedFragment : BindingFragment(R.layout.fra mainViewModel.getUserState.collect { state -> when (state) { is UiState.Success -> { - isGoalValid(state.data) + val data = dataStoreRepository.getUserInfo().firstOrNull() + isGoalValid(data) } is UiState.Failure -> { @@ -155,8 +163,8 @@ class WineyFeedFragment : BindingFragment(R.layout.fra } } - private fun isGoalValid(data: User) { - if (data.isOver) { + private fun isGoalValid(data: User?) { + if (data?.isOver == true) { wineyFeedDialogFragment = WineyFeedDialogFragment() wineyFeedDialogFragment.show(parentFragmentManager, TAG_WINEYFEED_DIALOG) } else { diff --git a/app/src/main/java/com/android/go/sopt/winey/presentation/main/mypage/MyPageFragment.kt b/app/src/main/java/com/android/go/sopt/winey/presentation/main/mypage/MyPageFragment.kt index 2f11bec3..9c98c8bf 100644 --- a/app/src/main/java/com/android/go/sopt/winey/presentation/main/mypage/MyPageFragment.kt +++ b/app/src/main/java/com/android/go/sopt/winey/presentation/main/mypage/MyPageFragment.kt @@ -8,10 +8,12 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.fragment.app.commit import androidx.fragment.app.replace +import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import com.android.go.sopt.winey.R import com.android.go.sopt.winey.databinding.FragmentMyPageBinding import com.android.go.sopt.winey.domain.entity.User +import com.android.go.sopt.winey.domain.repository.DataStoreRepository import com.android.go.sopt.winey.presentation.main.MainViewModel import com.android.go.sopt.winey.presentation.main.mypage.myfeed.MyFeedFragment import com.android.go.sopt.winey.util.binding.BindingFragment @@ -19,23 +21,28 @@ import com.android.go.sopt.winey.util.fragment.snackBar import com.android.go.sopt.winey.util.view.UiState import com.android.go.sopt.winey.util.view.setOnSingleClickListener import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import javax.inject.Inject @AndroidEntryPoint class MyPageFragment : BindingFragment(R.layout.fragment_my_page) { private val viewModel by activityViewModels() + @Inject + lateinit var dataStoreRepository: DataStoreRepository override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) init1On1ButtonClickListener() initLevelHelpButtonClickListener() initToMyFeedButtonClickListener() setupGetUserState() + viewModel.getUser() } override fun onResume() { super.onResume() - viewModel.getUser() } private fun initToMyFeedButtonClickListener() { @@ -46,7 +53,7 @@ class MyPageFragment : BindingFragment(R.layout.fragment_ private fun init1On1ButtonClickListener() { binding.clMypageTo1on1.setOnClickListener { - val url = "https://open.kakao.com/o/s751Susf" + val url = ONE_ON_ONE_URL val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) startActivity(intent) } @@ -60,30 +67,29 @@ class MyPageFragment : BindingFragment(R.layout.fragment_ } private fun setupGetUserState() { - lifecycleScope.launch { - viewModel.getUserState.collect { state -> + viewModel.getUserState.flowWithLifecycle(lifecycle).onEach { state -> - when (state) { - is UiState.Loading -> { - } + when (state) { + is UiState.Loading -> { + } - is UiState.Success -> { - handleSuccessState(state.data) - handleTargetModifyButtonState(state.data) - } + is UiState.Success -> { + val data = dataStoreRepository.getUserInfo().firstOrNull() + handleSuccessState(data) + handleTargetModifyButtonState(data) + } - is UiState.Failure -> { - snackBar(binding.root) { state.msg } - } + is UiState.Failure -> { + snackBar(binding.root) { state.msg } + } - is UiState.Empty -> { - } + is UiState.Empty -> { } } - } + }.launchIn(lifecycleScope) } - private fun handleTargetModifyButtonState(data: User) { + private fun handleTargetModifyButtonState(data: User?) { binding.btnMypageTargetModify.setOnSingleClickListener { val bottomSheet = TargetAmountBottomSheetFragment() bottomSheet.show(this.childFragmentManager, bottomSheet.tag) @@ -101,10 +107,10 @@ class MyPageFragment : BindingFragment(R.layout.fragment_ } } - private fun handleSuccessState(data: User) { + private fun handleSuccessState(data: User?) { binding.data = data - when (data.isOver) { + when (data?.isOver) { true -> { binding.tvMypageTargetAmount.text = getString(R.string.mypage_not_yet_set) binding.tvMypagePeriodValue.text = getString(R.string.mypage_not_yet_set) @@ -119,8 +125,11 @@ class MyPageFragment : BindingFragment(R.layout.fragment_ binding.dday = data } } + + null -> { + } } - when (data.userLevel) { + when (data?.userLevel) { LEVEL_COMMON -> { binding.ivMypageProgressbar.setImageResource(R.drawable.ic_mypage_lv1_progressbar) binding.ivMypageProfile.setImageResource(R.drawable.ic_mypage_lv1_profile) @@ -157,5 +166,6 @@ class MyPageFragment : BindingFragment(R.layout.fragment_ private const val LEVEL_KNIGHT = "기사" private const val LEVEL_NOBLESS = "귀족" private const val LEVEL_KING = "황제" + private const val ONE_ON_ONE_URL = "https://open.kakao.com/o/s751Susf" } } diff --git a/app/src/main/java/com/android/go/sopt/winey/presentation/main/mypage/TargetAmountBottomSheetFragment.kt b/app/src/main/java/com/android/go/sopt/winey/presentation/main/mypage/TargetAmountBottomSheetFragment.kt index f0c11869..d504faa0 100644 --- a/app/src/main/java/com/android/go/sopt/winey/presentation/main/mypage/TargetAmountBottomSheetFragment.kt +++ b/app/src/main/java/com/android/go/sopt/winey/presentation/main/mypage/TargetAmountBottomSheetFragment.kt @@ -9,7 +9,7 @@ import android.view.ViewGroup import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels -import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.flowWithLifecycle import com.android.go.sopt.winey.R import com.android.go.sopt.winey.databinding.FragmentTargetAmountBottomSheetBinding import com.android.go.sopt.winey.presentation.main.MainViewModel @@ -17,10 +17,13 @@ import com.android.go.sopt.winey.util.binding.BindingBottomSheetDialogFragment import com.android.go.sopt.winey.util.context.colorOf import com.android.go.sopt.winey.util.context.hideKeyboard import com.android.go.sopt.winey.util.fragment.snackBar +import com.android.go.sopt.winey.util.fragment.viewLifeCycle +import com.android.go.sopt.winey.util.fragment.viewLifeCycleScope import com.android.go.sopt.winey.util.view.UiState import com.google.android.material.bottomsheet.BottomSheetBehavior import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import java.text.DecimalFormat @AndroidEntryPoint @@ -56,26 +59,24 @@ class TargetAmountBottomSheetFragment : } fun initCreateGoalObserver() { - lifecycleScope.launch { - viewModel.createGoalState.collect { - when (it) { - is UiState.Loading -> { - } + viewModel.createGoalState.flowWithLifecycle(viewLifeCycle).onEach { + when (it) { + is UiState.Loading -> { + } - is UiState.Success -> { - mainViewModel.getUser() - this@TargetAmountBottomSheetFragment.dismiss() - } + is UiState.Success -> { + mainViewModel.getUser() + this@TargetAmountBottomSheetFragment.dismiss() + } - is UiState.Failure -> { - snackBar(binding.root) { it.msg } - } + is UiState.Failure -> { + snackBar(binding.root) { it.msg } + } - is UiState.Empty -> { - } + is UiState.Empty -> { } } - } + }.launchIn(viewLifeCycleScope) } fun initCancelButtonClickListener() { @@ -141,75 +142,69 @@ class TargetAmountBottomSheetFragment : } private fun initAmountCheckObserver() { - lifecycleScope.launch { - viewModel.amountCheck.collect { - when (it) { - true -> { - binding.tilTargetAmountSetAmount.error = " " - binding.etTargetAmountSetAmount.setTextColor( - requireContext().colorOf(R.color.red_500) - ) - binding.tvTargetAmountWarningAmount.setTextColor( - requireContext().colorOf(R.color.red_500) - ) - } + viewModel.amountCheck.flowWithLifecycle(viewLifeCycle).onEach { + when (it) { + true -> { + binding.tilTargetAmountSetAmount.error = " " + binding.etTargetAmountSetAmount.setTextColor( + requireContext().colorOf(R.color.red_500) + ) + binding.tvTargetAmountWarningAmount.setTextColor( + requireContext().colorOf(R.color.red_500) + ) + } - false -> { - binding.tilTargetAmountSetAmount.error = null - binding.etTargetAmountSetAmount.setTextColor( - requireContext().colorOf(R.color.purple_400) - ) - binding.tvTargetAmountWarningAmount.setTextColor( - requireContext().colorOf(R.color.gray_400) - ) - } + false -> { + binding.tilTargetAmountSetAmount.error = null + binding.etTargetAmountSetAmount.setTextColor( + requireContext().colorOf(R.color.purple_400) + ) + binding.tvTargetAmountWarningAmount.setTextColor( + requireContext().colorOf(R.color.gray_400) + ) } } - } + }.launchIn(viewLifeCycleScope) } private fun initDayCheckObserver() { - lifecycleScope.launch { - viewModel.dayCheck.collect { - when (it) { - true -> { - binding.tilTargetAmountSetDay.error = " " - binding.etTargetAmountSetDay.setTextColor( - requireContext().colorOf(R.color.red_500) - ) - binding.tvTargetAmountWarningDay.setTextColor( - requireContext().colorOf(R.color.red_500) - ) - } + viewModel.dayCheck.flowWithLifecycle(viewLifeCycle).onEach { + when (it) { + true -> { + binding.tilTargetAmountSetDay.error = " " + binding.etTargetAmountSetDay.setTextColor( + requireContext().colorOf(R.color.red_500) + ) + binding.tvTargetAmountWarningDay.setTextColor( + requireContext().colorOf(R.color.red_500) + ) + } - false -> { - binding.tilTargetAmountSetDay.error = null - binding.etTargetAmountSetDay.setTextColor( - requireContext().colorOf(R.color.purple_400) - ) - binding.tvTargetAmountWarningDay.setTextColor( - requireContext().colorOf(R.color.gray_400) - ) - } + false -> { + binding.tilTargetAmountSetDay.error = null + binding.etTargetAmountSetDay.setTextColor( + requireContext().colorOf(R.color.purple_400) + ) + binding.tvTargetAmountWarningDay.setTextColor( + requireContext().colorOf(R.color.gray_400) + ) } } - } + }.launchIn(viewLifeCycleScope) } private fun initButtonStateCheckObserver() { - lifecycleScope.launch { - viewModel.buttonStateCheck.collect { - when (it) { - true -> { - binding.btnTargetAmountSave.isEnabled = true - } + viewModel.buttonStateCheck.flowWithLifecycle(viewLifeCycle).onEach { + when (it) { + true -> { + binding.btnTargetAmountSave.isEnabled = true + } - false -> { - binding.btnTargetAmountSave.isEnabled = false - } + false -> { + binding.btnTargetAmountSave.isEnabled = false } } - } + }.launchIn(viewLifeCycleScope) } private fun makeCommaString(input: Long): String { diff --git a/app/src/main/java/com/android/go/sopt/winey/presentation/main/mypage/TargetAmountViewModel.kt b/app/src/main/java/com/android/go/sopt/winey/presentation/main/mypage/TargetAmountViewModel.kt index 77756233..a97e825a 100644 --- a/app/src/main/java/com/android/go/sopt/winey/presentation/main/mypage/TargetAmountViewModel.kt +++ b/app/src/main/java/com/android/go/sopt/winey/presentation/main/mypage/TargetAmountViewModel.kt @@ -28,16 +28,16 @@ class TargetAmountViewModel @Inject constructor( val day: LiveData get() = _day private val _amountCheck = MutableStateFlow(false) - val amountCheck: StateFlow get() = _amountCheck.asStateFlow() + val amountCheck: StateFlow = _amountCheck.asStateFlow() private val _dayCheck = MutableStateFlow(false) - val dayCheck: StateFlow get() = _dayCheck.asStateFlow() + val dayCheck: StateFlow = _dayCheck.asStateFlow() private val _buttonStatecheck = MutableStateFlow(false) - val buttonStateCheck: StateFlow get() = _buttonStatecheck.asStateFlow() + val buttonStateCheck: StateFlow = _buttonStatecheck.asStateFlow() - private val _createGoalState = MutableStateFlow>(UiState.Empty) - val createGoalState: StateFlow> get() = _createGoalState.asStateFlow() + private val _createGoalState = MutableStateFlow>(UiState.Empty) + val createGoalState: StateFlow> = _createGoalState.asStateFlow() fun postCreateGoal() { val money: Any? = amount.value @@ -93,7 +93,7 @@ class TargetAmountViewModel @Inject constructor( val day = _day.value val amount = _amount.value _buttonStatecheck.value = - !day.isNullOrEmpty() && !amount.isNullOrEmpty() && _dayCheck.value && _amountCheck.value + !day.isNullOrEmpty() && !amount.isNullOrEmpty() && !_dayCheck.value && !_amountCheck.value } companion object { diff --git a/app/src/main/java/com/android/go/sopt/winey/presentation/main/recommend/RecommendFragment.kt b/app/src/main/java/com/android/go/sopt/winey/presentation/main/recommend/RecommendFragment.kt index 64e4390d..77ea8ea6 100644 --- a/app/src/main/java/com/android/go/sopt/winey/presentation/main/recommend/RecommendFragment.kt +++ b/app/src/main/java/com/android/go/sopt/winey/presentation/main/recommend/RecommendFragment.kt @@ -3,6 +3,7 @@ package com.android.go.sopt.winey.presentation.main.recommend import android.os.Bundle import android.view.View import androidx.fragment.app.viewModels +import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.ConcatAdapter import com.android.go.sopt.winey.R @@ -12,7 +13,8 @@ import com.android.go.sopt.winey.util.binding.BindingFragment import com.android.go.sopt.winey.util.fragment.snackBar import com.android.go.sopt.winey.util.view.UiState import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach @AndroidEntryPoint class RecommendFragment : BindingFragment(R.layout.fragment_recommend) { @@ -35,24 +37,22 @@ class RecommendFragment : BindingFragment(R.layout.fra } private fun getRecommendListStateObserver() { - lifecycleScope.launch { - viewModel.getRecommendListState.collect { state -> - when (state) { - is UiState.Loading -> { - } - - is UiState.Success -> { - recommendAdapter.submitList(state.data) - } - - is UiState.Failure -> { - snackBar(binding.root) { state.msg } - } - - is UiState.Empty -> { - } + viewModel.getRecommendListState.flowWithLifecycle(lifecycle).onEach { state -> + when (state) { + is UiState.Loading -> { + } + + is UiState.Success -> { + recommendAdapter.submitList(state.data) + } + + is UiState.Failure -> { + snackBar(binding.root) { state.msg } + } + + is UiState.Empty -> { } } - } + }.launchIn(lifecycleScope) } } diff --git a/app/src/main/java/com/android/go/sopt/winey/presentation/main/recommend/RecommendViewModel.kt b/app/src/main/java/com/android/go/sopt/winey/presentation/main/recommend/RecommendViewModel.kt index 570825bd..ad70953d 100644 --- a/app/src/main/java/com/android/go/sopt/winey/presentation/main/recommend/RecommendViewModel.kt +++ b/app/src/main/java/com/android/go/sopt/winey/presentation/main/recommend/RecommendViewModel.kt @@ -18,8 +18,8 @@ import javax.inject.Inject class RecommendViewModel @Inject constructor( private val authRepository: AuthRepository ) : ViewModel() { - private val _getRecommendListState = MutableStateFlow>>(UiState.Loading) - val getRecommendListState: StateFlow>> = + private val _getRecommendListState = MutableStateFlow?>>(UiState.Loading) + val getRecommendListState: StateFlow?>> = _getRecommendListState.asStateFlow() init { diff --git a/app/src/main/java/com/android/go/sopt/winey/presentation/onboarding/KakaoLoginCallback.kt b/app/src/main/java/com/android/go/sopt/winey/presentation/onboarding/KakaoLoginCallback.kt new file mode 100644 index 00000000..aa22fab8 --- /dev/null +++ b/app/src/main/java/com/android/go/sopt/winey/presentation/onboarding/KakaoLoginCallback.kt @@ -0,0 +1,71 @@ +package com.android.go.sopt.winey.presentation.onboarding + +import com.kakao.sdk.auth.model.OAuthToken +import com.kakao.sdk.common.model.AuthErrorCause +import com.kakao.sdk.common.model.ClientError +import com.kakao.sdk.common.model.ClientErrorCause +import timber.log.Timber + +class KakaoLoginCallback(private val onSuccess: (accessToken: String) -> Unit) { + fun handleResult(token: OAuthToken?, error: Throwable?) { + if (error != null) { + when { + error.toString() == AuthErrorCause.AccessDenied.toString() -> { + Timber.e(error, ACCESS_DENIED) + } + + error.toString() == AuthErrorCause.InvalidClient.toString() -> { + Timber.e(error, INVALID_CLIENT) + } + + error.toString() == AuthErrorCause.InvalidGrant.toString() -> { + Timber.e(error, INVALID_GRANT) + } + + error.toString() == AuthErrorCause.InvalidRequest.toString() -> { + Timber.e(error, INVALID_REQUEST) + } + + error.toString() == AuthErrorCause.InvalidScope.toString() -> { + Timber.e(error, INVALID_SCOPE) + } + + error.toString() == AuthErrorCause.Misconfigured.toString() -> { + Timber.e(error, MISCONFIGURED) + } + + error.toString() == AuthErrorCause.ServerError.toString() -> { + Timber.e(error, SERVER_ERROR) + } + + error.toString() == AuthErrorCause.Unauthorized.toString() -> { + Timber.e(error, UNAUTHORIZED) + } + + error is ClientError && error.reason == ClientErrorCause.Cancelled -> { + Timber.e(error, CANCELLED) + } + + else -> { // + Timber.e(error, ELSE) + } + } + } else if (token != null) { + Timber.d(ON_SUCCESS) + onSuccess(token.accessToken) + } + } + companion object { + private const val ACCESS_DENIED = "접근이 거부 됨(동의 취소)" + private const val INVALID_CLIENT = "유효하지 않은 앱" + private const val INVALID_GRANT = "인증 수단이 유효하지 않아 인증할 수 없는 상태" + private const val INVALID_REQUEST = "요청 파라미터 오류" + private const val INVALID_SCOPE = "유효하지 않은 scope ID" + private const val MISCONFIGURED = "설정이 올바르지 않음(android key hash)" + private const val SERVER_ERROR = "서버 내부 에러" + private const val UNAUTHORIZED = "앱이 요청 권한이 없음" + private const val CANCELLED = "의도적인 로그인 취소로 보고 카카오계정으로 로그인 시도 없이 로그인 취소로 처리" + private const val ELSE = "기타 에러" + private const val ON_SUCCESS = "로그인 성공" + } +} diff --git a/app/src/main/java/com/android/go/sopt/winey/presentation/onboarding/LoginActivity.kt b/app/src/main/java/com/android/go/sopt/winey/presentation/onboarding/LoginActivity.kt new file mode 100644 index 00000000..253a04c3 --- /dev/null +++ b/app/src/main/java/com/android/go/sopt/winey/presentation/onboarding/LoginActivity.kt @@ -0,0 +1,64 @@ +package com.android.go.sopt.winey.presentation.onboarding + +import android.content.Intent +import android.os.Bundle +import androidx.activity.viewModels +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import com.android.go.sopt.winey.R +import com.android.go.sopt.winey.databinding.ActivityLoginBinding +import com.android.go.sopt.winey.presentation.main.MainActivity +import com.android.go.sopt.winey.util.binding.BindingActivity +import com.android.go.sopt.winey.util.context.snackBar +import com.android.go.sopt.winey.util.view.UiState +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +@AndroidEntryPoint +class LoginActivity : + BindingActivity(R.layout.activity_login) { + val viewModel: LoginViewModel by viewModels() + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + initKakaoLoginButtonClickListener() + initLoginObserver() + } + + private fun initKakaoLoginButtonClickListener() { + binding.btnLoginKakao.setOnClickListener { + viewModel.loginKakao(this) + } + } + + private fun initLoginObserver() { + viewModel.loginState.flowWithLifecycle(lifecycle).onEach { state -> + when (state) { + is UiState.Loading -> { + binding.btnLoginKakao.isEnabled = false + } + + is UiState.Success -> { + if (state.data?.isRegistered == true) { + val intent = Intent(this@LoginActivity, MainActivity::class.java) + startActivity(intent) + finish() + } else { + //TODO : isRegistered false일경우 닉네임 설정화면으로 + val intent = Intent(this@LoginActivity, MainActivity::class.java) + startActivity(intent) + finish() + } + } + + is UiState.Failure -> { + snackBar(binding.root) { state.msg } + } + + is UiState.Empty -> { + } + } + }.launchIn(lifecycleScope) + } +} diff --git a/app/src/main/java/com/android/go/sopt/winey/presentation/onboarding/LoginViewModel.kt b/app/src/main/java/com/android/go/sopt/winey/presentation/onboarding/LoginViewModel.kt new file mode 100644 index 00000000..bff76e53 --- /dev/null +++ b/app/src/main/java/com/android/go/sopt/winey/presentation/onboarding/LoginViewModel.kt @@ -0,0 +1,110 @@ +package com.android.go.sopt.winey.presentation.onboarding + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.android.go.sopt.winey.data.model.remote.request.RequestLoginDto +import com.android.go.sopt.winey.data.model.remote.response.ResponseLoginDto +import com.android.go.sopt.winey.domain.repository.AuthRepository +import com.android.go.sopt.winey.domain.repository.DataStoreRepository +import com.android.go.sopt.winey.domain.repository.KakaoLoginRepository +import com.android.go.sopt.winey.util.view.UiState +import com.kakao.sdk.auth.model.OAuthToken +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import retrofit2.HttpException +import timber.log.Timber +import javax.inject.Inject + +@HiltViewModel +class LoginViewModel @Inject constructor( + private val kakaoLoginRepository: KakaoLoginRepository, + private val authRepository: AuthRepository, + private val dataStoreRepository: DataStoreRepository +) : ViewModel() { + private val _isKakaoLogin = MutableStateFlow(false) + val isKakaoLogin = _isKakaoLogin.asStateFlow() + + private val _loginState = MutableStateFlow>(UiState.Empty) + val loginState: StateFlow> = _loginState.asStateFlow() + + val kakaoLoginCallback: (OAuthToken?, Throwable?) -> Unit = { token, error -> + KakaoLoginCallback { + _isKakaoLogin.value = true + Timber.d("액세스토큰 ${token?.accessToken}") + Timber.d("리프레시토큰 ${token?.refreshToken}") + if (token != null) { + saveSocialToken(token.accessToken, token.refreshToken) + postLogin(token.accessToken, KAKAO) + } else { + Timber.e("token is null") + } + }.handleResult(token, error) + } + + fun loginKakao(context: Context) = viewModelScope.launch { + kakaoLoginRepository.loginKakao(kakaoLoginCallback, context) + } + + fun postLogin(socialToken: String, socialType: String) { + viewModelScope.launch { + _loginState.value = UiState.Loading + + authRepository.postLogin(socialToken, RequestLoginDto(socialType)) + .onSuccess { response -> + Timber.e("로그인 성공") + if (response != null) { + saveAccessToken(response.accessToken, response.refreshToken) + saveUserId(response.userId) + _loginState.value = UiState.Success(response) + } else { + Timber.e("response is null") + } + } + .onFailure { t -> + if (t is HttpException) { + Timber.e("HTTP 실패 ${t.code()}, ${t.message()}") + } + Timber.e("${t.message}") + _loginState.value = UiState.Failure("${t.message}") + } + } + } + + fun saveSocialToken(socialAccessToken: String, socialRefreshToken: String) = + viewModelScope.launch(Dispatchers.IO) { + dataStoreRepository.saveSocialToken(socialAccessToken, socialRefreshToken) + } + + suspend fun getSocialToken() = withContext(Dispatchers.IO) { + dataStoreRepository.getSocialAccessToken().first() + } + + fun saveAccessToken(accessToken: String, refreshToken: String) = + viewModelScope.launch(Dispatchers.IO) { + dataStoreRepository.saveAccessToken(accessToken, refreshToken) + } + + fun saveUserId(userId: Int) = + viewModelScope.launch(Dispatchers.IO) { + dataStoreRepository.saveUserId(userId) + } + + suspend fun getAccessToken() = withContext(Dispatchers.IO) { + dataStoreRepository.getAccessToken().first() + } + + suspend fun getRefreshToken() = withContext(Dispatchers.IO) { + dataStoreRepository.getRefreshToken().first() + } + + companion object { + private const val KAKAO = "KAKAO" + } +} diff --git a/app/src/main/java/com/android/go/sopt/winey/presentation/splash/SplashActivity.kt b/app/src/main/java/com/android/go/sopt/winey/presentation/splash/SplashActivity.kt index ff85ca41..99fd9617 100644 --- a/app/src/main/java/com/android/go/sopt/winey/presentation/splash/SplashActivity.kt +++ b/app/src/main/java/com/android/go/sopt/winey/presentation/splash/SplashActivity.kt @@ -5,23 +5,47 @@ import android.os.Bundle import androidx.lifecycle.lifecycleScope import com.android.go.sopt.winey.R import com.android.go.sopt.winey.databinding.ActivitySplashBinding +import com.android.go.sopt.winey.domain.repository.DataStoreRepository import com.android.go.sopt.winey.presentation.main.MainActivity +import com.android.go.sopt.winey.presentation.onboarding.LoginActivity import com.android.go.sopt.winey.util.binding.BindingActivity import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import javax.inject.Inject @AndroidEntryPoint class SplashActivity : BindingActivity(R.layout.activity_splash) { + @Inject + lateinit var dataStoreRepository: DataStoreRepository override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) lifecycleScope.launch { delay(DELAY_TIME) + checkAutoLogin() + } + } + + private fun checkAutoLogin() { + val accessToken = runBlocking { dataStoreRepository.getAccessToken().firstOrNull() } + if (accessToken.isNullOrBlank()) { + navigateToOnBoardingScreen() + } else { navigateToMainScreen() } } + private fun navigateToOnBoardingScreen() { + Intent(this@SplashActivity, LoginActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + startActivity(this) + } + finish() + } + private fun navigateToMainScreen() { Intent(this@SplashActivity, MainActivity::class.java).apply { addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) diff --git a/app/src/main/res/drawable/ic_login_kakao.xml b/app/src/main/res/drawable/ic_login_kakao.xml new file mode 100644 index 00000000..9d3c0ed3 --- /dev/null +++ b/app/src/main/res/drawable/ic_login_kakao.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/drawable/ic_login_saver.xml b/app/src/main/res/drawable/ic_login_saver.xml new file mode 100644 index 00000000..1a9ba337 --- /dev/null +++ b/app/src/main/res/drawable/ic_login_saver.xml @@ -0,0 +1,289 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_login_title.xml b/app/src/main/res/drawable/ic_login_title.xml new file mode 100644 index 00000000..43baf68b --- /dev/null +++ b/app/src/main/res/drawable/ic_login_title.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml new file mode 100644 index 00000000..9682480b --- /dev/null +++ b/app/src/main/res/layout/activity_login.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5b0bac15..f36c07a9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -92,5 +92,9 @@ 0 위니가 알려주는 추천 절약법 쉿, 그대를 위한 절약방법이 도착했어.\n황실 내에서도 비밀인데 특별히 알려줄게:) + 6자 이상 작성해주세요 + + 절약을 더 쉽고 재밌게 + 카카오톡으로 3초만에 시작하기 diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index 25e0c8e9..9a8f8e13 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -31,6 +31,8 @@ object AndroidXDependencies { const val workManager = "androidx.work:work-runtime-ktx:${Versions.workManagerVersion}" const val hiltWorkManager = "androidx.hilt:hilt-work:1.0.0" const val exif = "androidx.exifinterface:exifinterface:${Versions.exifVersion}" + const val dataStore = "androidx.datastore:datastore-preferences:${Versions.dataStoreVersion}" + const val dataStoreCore = "androidx.datastore:datastore-preferences-core:${Versions.dataStoreVersion}" } object TestDependencies { @@ -75,6 +77,7 @@ object ThirdPartyDependencies { "com.squareup.leakcanary:leakcanary-android:${Versions.leakCanaryVersion}" const val soloader = "com.facebook.soloader:soloader:${Versions.soloaderVersion}" const val circleImageView = "de.hdodenhof:circleimageview:${Versions.circleImageViewVersion}" + const val kakaoLogin = "com.kakao.sdk:v2-user:${Versions.kakaoLoginVersion}" } object FirebaseDependencies { diff --git a/buildSrc/src/main/java/Versions.kt b/buildSrc/src/main/java/Versions.kt index 1036ba22..ce0113c1 100644 --- a/buildSrc/src/main/java/Versions.kt +++ b/buildSrc/src/main/java/Versions.kt @@ -26,6 +26,7 @@ object Versions { const val splashVersion = "1.0.1" const val workManagerVersion = "2.8.1" const val exifVersion = "1.3.2" + const val dataStoreVersion = "1.0.0" const val coilVersion = "2.4.0" const val retrofitVersion = "2.9.0" @@ -41,7 +42,8 @@ object Versions { const val leakCanaryVersion = "2.11" const val circleImageViewVersion = "3.1.0" + const val kakaoLoginVersion = "2.10.0" val javaVersion = JavaVersion.VERSION_17 const val jvmVersion = "17" -} \ No newline at end of file +} diff --git a/settings.gradle.kts b/settings.gradle.kts index cc9a4639..5f0d6b0b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -10,6 +10,7 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + maven("https://devrepo.kakao.com/nexus/content/groups/public/") } } rootProject.name = "Winey"