diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a18a009..3b85493 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -5,6 +5,8 @@ plugins { alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.hilt.android) + id("kotlin-kapt") } val properties = Properties().apply { @@ -46,9 +48,18 @@ android { compose = true buildConfig = true } + + hilt { + enableAggregatingTask = false + } } dependencies { + // Hilt + implementation(libs.hilt.android) + implementation(libs.hilt.navigation.compose) + implementation(libs.androidx.appcompat) + kapt(libs.hilt.compiler) // Network implementation(platform(libs.okhttp.bom)) implementation(libs.okhttp) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7dcf9b7..b5a93b8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,6 +5,7 @@ diff --git a/app/src/main/java/org/sopt/and/WavveApp.kt b/app/src/main/java/org/sopt/and/WavveApp.kt new file mode 100644 index 0000000..e16ed15 --- /dev/null +++ b/app/src/main/java/org/sopt/and/WavveApp.kt @@ -0,0 +1,17 @@ +package org.sopt.and + +import android.app.Application +import androidx.appcompat.app.AppCompatDelegate +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class WavveApp: Application() { + override fun onCreate() { + super.onCreate() + setDarkMode() + } + + private fun setDarkMode() { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/data/ApiFactory.kt b/app/src/main/java/org/sopt/and/data/ApiFactory.kt deleted file mode 100644 index 4ad4168..0000000 --- a/app/src/main/java/org/sopt/and/data/ApiFactory.kt +++ /dev/null @@ -1,36 +0,0 @@ -package org.sopt.and.data - -import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory -import kotlinx.serialization.json.Json -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor -import org.sopt.and.BuildConfig -import org.sopt.and.data.network.service.UserService -import retrofit2.Retrofit - -object ApiFactory { - private const val BASE_URL: String = BuildConfig.BASE_URL - - private val loggingInterceptor = HttpLoggingInterceptor().apply { - level = HttpLoggingInterceptor.Level.BODY - } - - private val client = OkHttpClient.Builder() - .addInterceptor(loggingInterceptor) - .build() - - val retrofit: Retrofit by lazy { - Retrofit.Builder() - .baseUrl(BASE_URL) - .client(client) - .addConverterFactory(Json.asConverterFactory("application/json".toMediaType())) - .build() - } - - inline fun create(): T = retrofit.create(T::class.java) -} - -object ServicePool { - val userService = ApiFactory.create() -} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/data/remote/model/TodayTopData.kt b/app/src/main/java/org/sopt/and/data/local/TodayTopData.kt similarity index 64% rename from app/src/main/java/org/sopt/and/data/remote/model/TodayTopData.kt rename to app/src/main/java/org/sopt/and/data/local/TodayTopData.kt index 8ad0d84..dafa5c2 100644 --- a/app/src/main/java/org/sopt/and/data/remote/model/TodayTopData.kt +++ b/app/src/main/java/org/sopt/and/data/local/TodayTopData.kt @@ -1,4 +1,4 @@ -package org.sopt.and.data.remote.model +package org.sopt.and.data.local data class TodayTopData( val painterId: Int, diff --git a/app/src/main/java/org/sopt/and/data/mapper/todata/UserLoginModelMapper.kt b/app/src/main/java/org/sopt/and/data/mapper/todata/UserLoginModelMapper.kt new file mode 100644 index 0000000..47e6be0 --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/mapper/todata/UserLoginModelMapper.kt @@ -0,0 +1,9 @@ +package org.sopt.and.data.mapper.todata + +import org.sopt.and.data.remote.model.request.UserLoginRequestDto +import org.sopt.and.domain.model.request.UserLoginModel + +fun UserLoginModel.toData() : UserLoginRequestDto = UserLoginRequestDto( + username = this.username, + password = this.password +) \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/data/mapper/todata/UserSignUpModelMapper.kt b/app/src/main/java/org/sopt/and/data/mapper/todata/UserSignUpModelMapper.kt new file mode 100644 index 0000000..e5e58f9 --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/mapper/todata/UserSignUpModelMapper.kt @@ -0,0 +1,10 @@ +package org.sopt.and.data.mapper.todata + +import org.sopt.and.data.remote.model.request.UserSignUpRequestDto +import org.sopt.and.domain.model.request.UserSignUpModel + +fun UserSignUpModel.toData() : UserSignUpRequestDto = UserSignUpRequestDto( + username = this.username, + password = this.password, + hobby = this.hobby +) \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/data/mapper/todomain/UserHobbyResponseDtoMapper.kt b/app/src/main/java/org/sopt/and/data/mapper/todomain/UserHobbyResponseDtoMapper.kt new file mode 100644 index 0000000..6311a29 --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/mapper/todomain/UserHobbyResponseDtoMapper.kt @@ -0,0 +1,8 @@ +package org.sopt.and.data.mapper.todomain + +import org.sopt.and.data.remote.model.response.UserHobbyResponseDto +import org.sopt.and.domain.model.response.Hobby + +fun UserHobbyResponseDto.toDomain(): Hobby = Hobby( + hobby = this.hobby +) \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/data/mapper/todomain/UserSignUpResponseDtoMapper.kt b/app/src/main/java/org/sopt/and/data/mapper/todomain/UserSignUpResponseDtoMapper.kt new file mode 100644 index 0000000..5ed6022 --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/mapper/todomain/UserSignUpResponseDtoMapper.kt @@ -0,0 +1,8 @@ +package org.sopt.and.data.mapper.todomain + +import org.sopt.and.data.remote.model.response.UserSignUpResponseDto +import org.sopt.and.domain.model.response.UserNumber + +fun UserSignUpResponseDto.toDomain(): UserNumber = UserNumber( + userId = this.userId +) \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/data/mapper/todomain/UserTokenResponseDtoMapper.kt b/app/src/main/java/org/sopt/and/data/mapper/todomain/UserTokenResponseDtoMapper.kt new file mode 100644 index 0000000..51df841 --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/mapper/todomain/UserTokenResponseDtoMapper.kt @@ -0,0 +1,8 @@ +package org.sopt.and.data.mapper.todomain + +import org.sopt.and.data.remote.model.response.UserTokenResponseDto +import org.sopt.and.domain.model.response.Token + +fun UserTokenResponseDto.toDomain(): Token = Token( + token = this.token +) \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/data/remote/datasource/UserDataRemoteSource.kt b/app/src/main/java/org/sopt/and/data/remote/datasource/UserDataRemoteSource.kt new file mode 100644 index 0000000..6d49afc --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/remote/datasource/UserDataRemoteSource.kt @@ -0,0 +1,14 @@ +package org.sopt.and.data.remote.datasource + +import org.sopt.and.data.remote.model.base.ApiResponse +import org.sopt.and.data.remote.model.request.UserLoginRequestDto +import org.sopt.and.data.remote.model.request.UserSignUpRequestDto +import org.sopt.and.data.remote.model.response.UserHobbyResponseDto +import org.sopt.and.data.remote.model.response.UserSignUpResponseDto +import org.sopt.and.data.remote.model.response.UserTokenResponseDto + +interface UserDataRemoteSource { + suspend fun postUserSignUp(userSignUpRequestDto: UserSignUpRequestDto): ApiResponse + suspend fun postUserLogin(userLoginRequestDto: UserLoginRequestDto): ApiResponse + suspend fun getUserHobby(token: String): ApiResponse +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/data/remote/datasourceimpl/UserDataRemoteSourceImpl.kt b/app/src/main/java/org/sopt/and/data/remote/datasourceimpl/UserDataRemoteSourceImpl.kt new file mode 100644 index 0000000..9415dd2 --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/remote/datasourceimpl/UserDataRemoteSourceImpl.kt @@ -0,0 +1,26 @@ +package org.sopt.and.data.remote.datasourceimpl + +import org.sopt.and.data.remote.datasource.UserDataRemoteSource +import org.sopt.and.data.remote.model.base.ApiResponse +import org.sopt.and.data.remote.model.request.UserLoginRequestDto +import org.sopt.and.data.remote.model.request.UserSignUpRequestDto +import org.sopt.and.data.remote.model.response.UserHobbyResponseDto +import org.sopt.and.data.remote.model.response.UserSignUpResponseDto +import org.sopt.and.data.remote.model.response.UserTokenResponseDto +import org.sopt.and.data.remote.service.UserService +import javax.inject.Inject + +class UserDataRemoteSourceImpl @Inject constructor( + private val userService: UserService +) : UserDataRemoteSource { + + override suspend fun postUserSignUp(userSignUpRequestDto: UserSignUpRequestDto): ApiResponse = + userService.postUserSignUp(body = userSignUpRequestDto) + + override suspend fun postUserLogin(userLoginRequestDto: UserLoginRequestDto): ApiResponse = + userService.postUserLogin(body = userLoginRequestDto) + + + override suspend fun getUserHobby(token: String): ApiResponse = + userService.getUserHobby(token = token) +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/data/remote/model/response/UserSignUpResponseDto.kt b/app/src/main/java/org/sopt/and/data/remote/model/response/UserSignUpResponseDto.kt index 6609fd9..42aa942 100644 --- a/app/src/main/java/org/sopt/and/data/remote/model/response/UserSignUpResponseDto.kt +++ b/app/src/main/java/org/sopt/and/data/remote/model/response/UserSignUpResponseDto.kt @@ -7,5 +7,5 @@ import kotlinx.serialization.Serializable @Serializable data class UserSignUpResponseDto( @SerialName("no") - val no: Int + val userId: Int ) \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/data/network/service/UserService.kt b/app/src/main/java/org/sopt/and/data/remote/service/UserService.kt similarity index 95% rename from app/src/main/java/org/sopt/and/data/network/service/UserService.kt rename to app/src/main/java/org/sopt/and/data/remote/service/UserService.kt index 7f5013a..18cbf9b 100644 --- a/app/src/main/java/org/sopt/and/data/network/service/UserService.kt +++ b/app/src/main/java/org/sopt/and/data/remote/service/UserService.kt @@ -1,4 +1,4 @@ -package org.sopt.and.data.network.service +package org.sopt.and.data.remote.service import org.sopt.and.data.remote.model.base.ApiResponse import org.sopt.and.data.remote.model.request.UserLoginRequestDto diff --git a/app/src/main/java/org/sopt/and/data/remote/utils/ApiResponseHandler.kt b/app/src/main/java/org/sopt/and/data/remote/utils/ApiResponseHandler.kt new file mode 100644 index 0000000..f6f19d3 --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/remote/utils/ApiResponseHandler.kt @@ -0,0 +1,11 @@ +package org.sopt.and.data.remote.utils + +import org.sopt.and.data.remote.model.base.ApiResponse + +fun ApiResponse.handleApiResponse(): Result { + return try { + Result.success(this.result) + } catch (e: Exception) { + Result.failure(e) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/data/repositoryimpl/UserRepositoryImpl.kt b/app/src/main/java/org/sopt/and/data/repositoryimpl/UserRepositoryImpl.kt new file mode 100644 index 0000000..989aeb2 --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/repositoryimpl/UserRepositoryImpl.kt @@ -0,0 +1,36 @@ +package org.sopt.and.data.repositoryimpl + +import org.sopt.and.data.mapper.todata.toData +import org.sopt.and.data.mapper.todomain.toDomain +import org.sopt.and.data.remote.datasource.UserDataRemoteSource +import org.sopt.and.data.remote.utils.handleApiResponse +import org.sopt.and.domain.model.request.UserLoginModel +import org.sopt.and.domain.model.request.UserSignUpModel +import org.sopt.and.domain.model.response.Hobby +import org.sopt.and.domain.model.response.Token +import org.sopt.and.domain.model.response.UserNumber +import org.sopt.and.domain.repository.UserRepository +import javax.inject.Inject + +class UserRepositoryImpl @Inject constructor( + private val userDataRemoteSource: UserDataRemoteSource +) : UserRepository { + + override suspend fun postUserSignUp(userSignUpModel: UserSignUpModel): Result { + return runCatching { + userDataRemoteSource.postUserSignUp(userSignUpModel.toData()).handleApiResponse().getOrThrow().toDomain() + } + } + + override suspend fun postUserLogin(userLoginModel: UserLoginModel): Result { + return runCatching { + userDataRemoteSource.postUserLogin(userLoginModel.toData()).handleApiResponse().getOrThrow().toDomain() + } + } + + override suspend fun getUserHobby(token: String): Result { + return runCatching { + userDataRemoteSource.getUserHobby(token = token).handleApiResponse().getOrThrow().toDomain() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/di/DataSourceModule.kt b/app/src/main/java/org/sopt/and/di/DataSourceModule.kt new file mode 100644 index 0000000..ed50f7f --- /dev/null +++ b/app/src/main/java/org/sopt/and/di/DataSourceModule.kt @@ -0,0 +1,19 @@ +package org.sopt.and.di + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import org.sopt.and.data.remote.datasource.UserDataRemoteSource +import org.sopt.and.data.remote.datasourceimpl.UserDataRemoteSourceImpl +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class DataSourceModule { + @Binds + @Singleton + abstract fun bindUserDataRemoteSource( + userDataRemoteSourceImpl: UserDataRemoteSourceImpl + ): UserDataRemoteSource +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/di/NetworkModule.kt b/app/src/main/java/org/sopt/and/di/NetworkModule.kt new file mode 100644 index 0000000..22ed31f --- /dev/null +++ b/app/src/main/java/org/sopt/and/di/NetworkModule.kt @@ -0,0 +1,72 @@ +package org.sopt.and.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.Interceptor +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import org.sopt.and.BuildConfig +import retrofit2.Retrofit +import java.util.concurrent.TimeUnit +import javax.inject.Singleton + + +@Module +@InstallIn(SingletonComponent::class) +object NetworkModule { + @Provides + @Singleton + fun providesOkHttpClient( + loggingInterceptor: HttpLoggingInterceptor, + authInterceptor: Interceptor + ): OkHttpClient = + OkHttpClient.Builder().apply { + connectTimeout(10, TimeUnit.SECONDS) + writeTimeout(10, TimeUnit.SECONDS) + readTimeout(10, TimeUnit.SECONDS) + addInterceptor(authInterceptor) + }.build() + + @Provides + @Singleton + fun providesJson(): Json = + Json { + isLenient = true + prettyPrint = true + explicitNulls = false + ignoreUnknownKeys = true + } + + @Provides + @Singleton + fun provideLoggingInterceptor(): HttpLoggingInterceptor = HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + } + + @Provides + @Singleton + fun provideAuthInterceptor(): Interceptor = Interceptor { chain -> + val request = chain.request().newBuilder() + .addHeader("Authorization", "Bearer ") + .build() + chain.proceed(request) + } + + @Provides + @Singleton + fun provideWavveRetrofit( + okHttpClient: OkHttpClient, + json: Json + ): Retrofit { + return Retrofit.Builder() + .baseUrl(BuildConfig.BASE_URL) + .client(okHttpClient) + .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) + .build() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/di/RepositoryModule.kt b/app/src/main/java/org/sopt/and/di/RepositoryModule.kt new file mode 100644 index 0000000..398f170 --- /dev/null +++ b/app/src/main/java/org/sopt/and/di/RepositoryModule.kt @@ -0,0 +1,20 @@ +package org.sopt.and.di + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import org.sopt.and.data.repositoryimpl.UserRepositoryImpl +import org.sopt.and.domain.repository.UserRepository +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class RepositoryModule { + @Binds + @Singleton + abstract fun bindUserRepository( + userRepositoryImpl: UserRepositoryImpl + ): UserRepository + +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/di/ServiceModule.kt b/app/src/main/java/org/sopt/and/di/ServiceModule.kt new file mode 100644 index 0000000..6afb689 --- /dev/null +++ b/app/src/main/java/org/sopt/and/di/ServiceModule.kt @@ -0,0 +1,18 @@ +package org.sopt.and.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import org.sopt.and.data.remote.service.UserService +import retrofit2.Retrofit +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object ServiceModule { + @Provides + @Singleton + fun provideUserService(retrofit: Retrofit): UserService = + retrofit.create(UserService::class.java) +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/domain/model/request/UserLoginModel.kt b/app/src/main/java/org/sopt/and/domain/model/request/UserLoginModel.kt new file mode 100644 index 0000000..fe142ba --- /dev/null +++ b/app/src/main/java/org/sopt/and/domain/model/request/UserLoginModel.kt @@ -0,0 +1,6 @@ +package org.sopt.and.domain.model.request + +data class UserLoginModel( + val username: String, + val password: String +) diff --git a/app/src/main/java/org/sopt/and/domain/model/request/UserSignUpModel.kt b/app/src/main/java/org/sopt/and/domain/model/request/UserSignUpModel.kt new file mode 100644 index 0000000..e725d98 --- /dev/null +++ b/app/src/main/java/org/sopt/and/domain/model/request/UserSignUpModel.kt @@ -0,0 +1,7 @@ +package org.sopt.and.domain.model.request + +data class UserSignUpModel( + val username: String, + val password: String, + val hobby: String +) diff --git a/app/src/main/java/org/sopt/and/domain/model/response/Hobby.kt b/app/src/main/java/org/sopt/and/domain/model/response/Hobby.kt new file mode 100644 index 0000000..a9972da --- /dev/null +++ b/app/src/main/java/org/sopt/and/domain/model/response/Hobby.kt @@ -0,0 +1,5 @@ +package org.sopt.and.domain.model.response + +data class Hobby( + val hobby: String +) diff --git a/app/src/main/java/org/sopt/and/domain/model/response/Token.kt b/app/src/main/java/org/sopt/and/domain/model/response/Token.kt new file mode 100644 index 0000000..46abcc2 --- /dev/null +++ b/app/src/main/java/org/sopt/and/domain/model/response/Token.kt @@ -0,0 +1,5 @@ +package org.sopt.and.domain.model.response + +data class Token( + val token: String +) diff --git a/app/src/main/java/org/sopt/and/domain/model/response/UserNumber.kt b/app/src/main/java/org/sopt/and/domain/model/response/UserNumber.kt new file mode 100644 index 0000000..c951271 --- /dev/null +++ b/app/src/main/java/org/sopt/and/domain/model/response/UserNumber.kt @@ -0,0 +1,5 @@ +package org.sopt.and.domain.model.response + +data class UserNumber( + val userId: Int +) diff --git a/app/src/main/java/org/sopt/and/domain/repository/UserRepository.kt b/app/src/main/java/org/sopt/and/domain/repository/UserRepository.kt new file mode 100644 index 0000000..d18fd2e --- /dev/null +++ b/app/src/main/java/org/sopt/and/domain/repository/UserRepository.kt @@ -0,0 +1,13 @@ +package org.sopt.and.domain.repository + +import org.sopt.and.domain.model.request.UserLoginModel +import org.sopt.and.domain.model.request.UserSignUpModel +import org.sopt.and.domain.model.response.Hobby +import org.sopt.and.domain.model.response.Token +import org.sopt.and.domain.model.response.UserNumber + +interface UserRepository { + suspend fun postUserSignUp(userSignUpModel: UserSignUpModel): Result + suspend fun postUserLogin(userLoginModel: UserLoginModel): Result + suspend fun getUserHobby(token: String): Result +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/domain/type/HomeTabType.kt b/app/src/main/java/org/sopt/and/domain/type/HomeTabType.kt new file mode 100644 index 0000000..29a92c0 --- /dev/null +++ b/app/src/main/java/org/sopt/and/domain/type/HomeTabType.kt @@ -0,0 +1,24 @@ +package org.sopt.and.domain.type + +enum class HomeTabType( + val text: String, +) { + NEW_CLASSIC( + text = "뉴클래식" + ), + DRAMA( + text = "드라마" + ), + VARIETY_SHOW( + text = "예능" + ), + MOVIE( + text = "영화" + ), + ANIMATION( + text = "애니" + ), + OVERSEAS_SERIES( + text = "해외시리즈" + ) +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/feature/home/HomeViewModel.kt b/app/src/main/java/org/sopt/and/feature/home/HomeViewModel.kt deleted file mode 100644 index caa788f..0000000 --- a/app/src/main/java/org/sopt/and/feature/home/HomeViewModel.kt +++ /dev/null @@ -1,36 +0,0 @@ -package org.sopt.and.feature.home - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import androidx.lifecycle.ViewModel -import org.sopt.and.R -import org.sopt.and.data.remote.model.TodayTopData - -class HomeViewModel : ViewModel() { - private val _homeTabText = listOf( - R.string.new_classic, - R.string.drama, - R.string.variety_show, - R.string.movie, - R.string.animation, - R.string.overseas_series - ) - - val homeTabText: List - @Composable - get() = _homeTabText.map { stringResource(it) } - - val editorDummy = List(20) { - TodayTopData( - painterId = R.drawable.iv_editor_recommended_work, - ranking = it + 1 - ) - } - - val top20Dummy = List(20) { - TodayTopData( - painterId = R.drawable.iv_today_top_20, - ranking = it + 1 - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/feature/login/LoginViewModel.kt b/app/src/main/java/org/sopt/and/feature/login/LoginViewModel.kt deleted file mode 100644 index a6b3f4f..0000000 --- a/app/src/main/java/org/sopt/and/feature/login/LoginViewModel.kt +++ /dev/null @@ -1,68 +0,0 @@ -package org.sopt.and.feature.login - -import android.content.Context -import android.util.Log -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.launch -import okio.IOException -import org.sopt.and.R -import org.sopt.and.data.ServicePool -import org.sopt.and.data.remote.model.request.UserLoginRequestDto -import org.sopt.and.utils.toast -import retrofit2.HttpException - -class LoginViewModel : ViewModel() { - private val userService by lazy { ServicePool.userService } - - private val _token = MutableLiveData(null) - val token: LiveData get() = _token - - fun postUserLogin( - context: Context, - body: UserLoginRequestDto, - ) { - viewModelScope.launch { - runCatching { - userService.postUserLogin(body = body) - }.onSuccess { response -> - _token.value = response.result.token - }.onFailure { error -> - when (error) { - is HttpException -> { - when (error.code()) { - 400 -> context.toast(context.getString(R.string.fail_to_login)) - 403 -> context.toast(context.getString(R.string.fail_to_login_invalid_password)) - } - } - - is IOException -> { - context.toast(context.getString(R.string.fail_to_network)) - } - - else -> { - context.toast(context.getString(R.string.fail_to_login)) - } - } - Log.e("postUserLoginError", error.toString()) - } - } - } - - private val _userName = MutableLiveData("") - val userName: LiveData get() = _userName - - private val _password = MutableLiveData("") - val password: LiveData get() = _password - - - fun setUserName(newName: String) { - _userName.value = newName - } - - fun setPassword(newPassword: String) { - _password.value = newPassword - } -} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/feature/mypage/MyViewModel.kt b/app/src/main/java/org/sopt/and/feature/mypage/MyViewModel.kt deleted file mode 100644 index d9f1ea5..0000000 --- a/app/src/main/java/org/sopt/and/feature/mypage/MyViewModel.kt +++ /dev/null @@ -1,35 +0,0 @@ -package org.sopt.and.feature.mypage - -import android.util.Log -import androidx.compose.runtime.State -import androidx.compose.runtime.mutableStateOf -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.launch -import org.sopt.and.data.ServicePool -import org.sopt.and.data.remote.model.response.UserHobbyResponseDto - -class MyViewModel : ViewModel() { - private val userService by lazy { ServicePool.userService } - - private val _userState = mutableStateOf(null) - val userState: State get() = _userState - - private val _hobby = MutableLiveData("") - val hobby: LiveData get() = _hobby - - fun getUserHobby(token: String) { - viewModelScope.launch { - runCatching { - userService.getUserHobby(token = token) - }.onSuccess { response -> - _userState.value = response.result - _hobby.value = response.result.hobby - }.onFailure { error -> - Log.e("getUserHobbyError", error.toString()) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/feature/search/SearchViewModel.kt b/app/src/main/java/org/sopt/and/feature/search/SearchViewModel.kt deleted file mode 100644 index f3cb802..0000000 --- a/app/src/main/java/org/sopt/and/feature/search/SearchViewModel.kt +++ /dev/null @@ -1,6 +0,0 @@ -package org.sopt.and.feature.search - -import androidx.lifecycle.ViewModel - -class SearchViewModel : ViewModel() { -} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/core/designsystem/component/Banner.kt b/app/src/main/java/org/sopt/and/presentation/core/component/Banner.kt similarity index 98% rename from app/src/main/java/org/sopt/and/core/designsystem/component/Banner.kt rename to app/src/main/java/org/sopt/and/presentation/core/component/Banner.kt index 7bac48d..d123010 100644 --- a/app/src/main/java/org/sopt/and/core/designsystem/component/Banner.kt +++ b/app/src/main/java/org/sopt/and/presentation/core/component/Banner.kt @@ -1,4 +1,4 @@ -package org.sopt.and.core.designsystem.component +package org.sopt.and.presentation.core.component import androidx.compose.foundation.Image import androidx.compose.foundation.background diff --git a/app/src/main/java/org/sopt/and/core/designsystem/component/CustomBox.kt b/app/src/main/java/org/sopt/and/presentation/core/component/CustomBox.kt similarity index 98% rename from app/src/main/java/org/sopt/and/core/designsystem/component/CustomBox.kt rename to app/src/main/java/org/sopt/and/presentation/core/component/CustomBox.kt index 27c81ee..297d41e 100644 --- a/app/src/main/java/org/sopt/and/core/designsystem/component/CustomBox.kt +++ b/app/src/main/java/org/sopt/and/presentation/core/component/CustomBox.kt @@ -1,4 +1,4 @@ -package org.sopt.and.core.designsystem.component +package org.sopt.and.presentation.core.component import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -36,7 +36,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import org.sopt.and.R -import org.sopt.and.data.remote.model.TodayTopData +import org.sopt.and.data.local.TodayTopData import org.sopt.and.ui.theme.WavveTheme @Composable diff --git a/app/src/main/java/org/sopt/and/core/designsystem/component/CustomButton.kt b/app/src/main/java/org/sopt/and/presentation/core/component/CustomButton.kt similarity index 98% rename from app/src/main/java/org/sopt/and/core/designsystem/component/CustomButton.kt rename to app/src/main/java/org/sopt/and/presentation/core/component/CustomButton.kt index ddb9d7f..8a5d964 100644 --- a/app/src/main/java/org/sopt/and/core/designsystem/component/CustomButton.kt +++ b/app/src/main/java/org/sopt/and/presentation/core/component/CustomButton.kt @@ -1,4 +1,4 @@ -package org.sopt.and.core.designsystem.component +package org.sopt.and.presentation.core.component import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement diff --git a/app/src/main/java/org/sopt/and/core/designsystem/component/CustomDialog.kt b/app/src/main/java/org/sopt/and/presentation/core/component/CustomDialog.kt similarity index 98% rename from app/src/main/java/org/sopt/and/core/designsystem/component/CustomDialog.kt rename to app/src/main/java/org/sopt/and/presentation/core/component/CustomDialog.kt index e1337ca..47eb3a0 100644 --- a/app/src/main/java/org/sopt/and/core/designsystem/component/CustomDialog.kt +++ b/app/src/main/java/org/sopt/and/presentation/core/component/CustomDialog.kt @@ -1,4 +1,4 @@ -package org.sopt.and.core.designsystem.component +package org.sopt.and.presentation.core.component import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer diff --git a/app/src/main/java/org/sopt/and/core/designsystem/component/CustomTabBar.kt b/app/src/main/java/org/sopt/and/presentation/core/component/CustomTabBar.kt similarity index 86% rename from app/src/main/java/org/sopt/and/core/designsystem/component/CustomTabBar.kt rename to app/src/main/java/org/sopt/and/presentation/core/component/CustomTabBar.kt index c71ae50..c7700c6 100644 --- a/app/src/main/java/org/sopt/and/core/designsystem/component/CustomTabBar.kt +++ b/app/src/main/java/org/sopt/and/presentation/core/component/CustomTabBar.kt @@ -1,4 +1,4 @@ -package org.sopt.and.core.designsystem.component +package org.sopt.and.presentation.core.component import androidx.compose.material3.ScrollableTabRow import androidx.compose.material3.Tab @@ -12,12 +12,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import org.sopt.and.domain.type.HomeTabType import org.sopt.and.ui.theme.WavveTheme @Composable fun WavveTabBar( - tabTitles: List, modifier: Modifier = Modifier + tabTitles: Array, modifier: Modifier = Modifier ) { var selectedTabIndex by remember { mutableIntStateOf(0) } @@ -35,7 +36,7 @@ fun WavveTabBar( onClick = { selectedTabIndex = index }, text = { Text( - text = tab, + text = tab.text, fontSize = 18.sp, color = if (selectedTabIndex == index) Color.White else Color.Gray ) diff --git a/app/src/main/java/org/sopt/and/core/designsystem/component/CustomTextField.kt b/app/src/main/java/org/sopt/and/presentation/core/component/CustomTextField.kt similarity index 97% rename from app/src/main/java/org/sopt/and/core/designsystem/component/CustomTextField.kt rename to app/src/main/java/org/sopt/and/presentation/core/component/CustomTextField.kt index a97cfd6..6cb7c28 100644 --- a/app/src/main/java/org/sopt/and/core/designsystem/component/CustomTextField.kt +++ b/app/src/main/java/org/sopt/and/presentation/core/component/CustomTextField.kt @@ -1,4 +1,4 @@ -package org.sopt.and.core.designsystem.component +package org.sopt.and.presentation.core.component import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions diff --git a/app/src/main/java/org/sopt/and/core/designsystem/component/SocialLoginButton.kt b/app/src/main/java/org/sopt/and/presentation/core/component/SocialLoginButton.kt similarity index 98% rename from app/src/main/java/org/sopt/and/core/designsystem/component/SocialLoginButton.kt rename to app/src/main/java/org/sopt/and/presentation/core/component/SocialLoginButton.kt index 5be59c4..14a178b 100644 --- a/app/src/main/java/org/sopt/and/core/designsystem/component/SocialLoginButton.kt +++ b/app/src/main/java/org/sopt/and/presentation/core/component/SocialLoginButton.kt @@ -1,4 +1,4 @@ -package org.sopt.and.core.designsystem.component +package org.sopt.and.presentation.core.component import androidx.annotation.DrawableRes import androidx.compose.foundation.Image diff --git a/app/src/main/java/org/sopt/and/feature/home/HomeScreen.kt b/app/src/main/java/org/sopt/and/presentation/home/HomeScreen.kt similarity index 93% rename from app/src/main/java/org/sopt/and/feature/home/HomeScreen.kt rename to app/src/main/java/org/sopt/and/presentation/home/HomeScreen.kt index bf4962d..7ae8d31 100644 --- a/app/src/main/java/org/sopt/and/feature/home/HomeScreen.kt +++ b/app/src/main/java/org/sopt/and/presentation/home/HomeScreen.kt @@ -1,4 +1,4 @@ -package org.sopt.and.feature.home +package org.sopt.and.presentation.home import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -36,18 +36,18 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController import org.sopt.and.R -import org.sopt.and.core.designsystem.component.Banner -import org.sopt.and.core.designsystem.component.EditorRecommendBox -import org.sopt.and.core.designsystem.component.TodayTop20Box -import org.sopt.and.core.designsystem.component.WavveTabBar +import org.sopt.and.presentation.core.component.Banner +import org.sopt.and.presentation.core.component.EditorRecommendBox +import org.sopt.and.presentation.core.component.TodayTop20Box +import org.sopt.and.presentation.core.component.WavveTabBar import org.sopt.and.ui.theme.WavveTheme @OptIn(ExperimentalMaterial3Api::class) @Composable -fun HomeScreen(navController: NavController, viewModel: HomeViewModel = viewModel()) { +fun HomeScreen(navController: NavController, viewModel: HomeViewModel = hiltViewModel()) { val scrollState = rememberScrollState() val pagerState = rememberPagerState( pageCount = { 6 } @@ -93,7 +93,7 @@ fun HomeScreen(navController: NavController, viewModel: HomeViewModel = viewMode .fillMaxSize() .verticalScroll(scrollState) ) { - WavveTabBar(viewModel.homeTabText) + WavveTabBar(tabTitles = viewModel.homeTabText) HorizontalPager( modifier = Modifier diff --git a/app/src/main/java/org/sopt/and/presentation/home/HomeViewModel.kt b/app/src/main/java/org/sopt/and/presentation/home/HomeViewModel.kt new file mode 100644 index 0000000..9d8f829 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/home/HomeViewModel.kt @@ -0,0 +1,28 @@ +package org.sopt.and.presentation.home + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import org.sopt.and.R +import org.sopt.and.data.local.TodayTopData +import org.sopt.and.domain.type.HomeTabType +import javax.inject.Inject + +@HiltViewModel +class HomeViewModel @Inject constructor( +) : ViewModel() { + val homeTabText = HomeTabType.entries.toTypedArray() + + val editorDummy = List(20) { + TodayTopData( + painterId = R.drawable.iv_editor_recommended_work, + ranking = it + 1 + ) + } + + val top20Dummy = List(20) { + TodayTopData( + painterId = R.drawable.iv_today_top_20, + ranking = it + 1 + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/feature/login/LoginScreen.kt b/app/src/main/java/org/sopt/and/presentation/login/LoginScreen.kt similarity index 90% rename from app/src/main/java/org/sopt/and/feature/login/LoginScreen.kt rename to app/src/main/java/org/sopt/and/presentation/login/LoginScreen.kt index bd3a0b3..05e4b23 100644 --- a/app/src/main/java/org/sopt/and/feature/login/LoginScreen.kt +++ b/app/src/main/java/org/sopt/and/presentation/login/LoginScreen.kt @@ -1,4 +1,4 @@ -package org.sopt.and.feature.login +package org.sopt.and.presentation.login import android.content.Context import androidx.activity.compose.LocalOnBackPressedDispatcherOwner @@ -29,6 +29,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.VerticalDivider import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf @@ -47,14 +48,14 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController import org.sopt.and.R -import org.sopt.and.core.designsystem.component.AuthTextField -import org.sopt.and.core.designsystem.component.SocialLoginButtonGroup -import org.sopt.and.core.designsystem.component.WavveLoginButton -import org.sopt.and.data.remote.model.request.UserLoginRequestDto -import org.sopt.and.feature.main.Routes +import org.sopt.and.domain.model.request.UserLoginModel +import org.sopt.and.presentation.core.component.AuthTextField +import org.sopt.and.presentation.core.component.SocialLoginButtonGroup +import org.sopt.and.presentation.core.component.WavveLoginButton +import org.sopt.and.presentation.main.Routes import org.sopt.and.ui.theme.WavveTheme import org.sopt.and.utils.noRippleClickable @@ -63,7 +64,7 @@ import org.sopt.and.utils.noRippleClickable fun LoginScreen( navController: NavController, onLoginSuccess: (String, String) -> Unit, - viewModel: LoginViewModel = viewModel() + viewModel: LoginViewModel = hiltViewModel() ) { val context = LocalContext.current val focusManager = LocalFocusManager.current @@ -72,6 +73,7 @@ fun LoginScreen( val editor = sharedPreferences.edit() + val loginState by viewModel.loginState.collectAsState() val token by viewModel.token.observeAsState("") val userName by viewModel.userName.observeAsState("") val password by viewModel.password.observeAsState("") @@ -178,17 +180,18 @@ fun LoginScreen( onClick = { viewModel.postUserLogin( context = context, - body = UserLoginRequestDto( + body = UserLoginModel( username = userName, - password = password + password = password, ), + onSuccess = { + editor.putString(context.getString(R.string.login_token), token) + editor.apply() + onLoginSuccess(userName, password) + //키보드 내리기 + focusManager.clearFocus() + } ) - - editor.putString(context.getString(R.string.login_token), token) - editor.apply() - onLoginSuccess(userName, password) - //키보드 내리기 - focusManager.clearFocus() } ) diff --git a/app/src/main/java/org/sopt/and/presentation/login/LoginState.kt b/app/src/main/java/org/sopt/and/presentation/login/LoginState.kt new file mode 100644 index 0000000..e1b246f --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/login/LoginState.kt @@ -0,0 +1,10 @@ +package org.sopt.and.presentation.login + +import org.sopt.and.domain.model.response.Token + +sealed class LoginState { + data object Idle: LoginState() + data object Loading: LoginState() + data class Success(val result: Token): LoginState() + data class Failure(val message: String): LoginState() +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/login/LoginViewModel.kt b/app/src/main/java/org/sopt/and/presentation/login/LoginViewModel.kt new file mode 100644 index 0000000..0d55dd9 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/login/LoginViewModel.kt @@ -0,0 +1,71 @@ +package org.sopt.and.presentation.login + +import android.content.Context +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.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import org.sopt.and.R +import org.sopt.and.domain.model.request.UserLoginModel +import org.sopt.and.domain.repository.UserRepository +import org.sopt.and.utils.handleErrorToast +import javax.inject.Inject + +@HiltViewModel +class LoginViewModel @Inject constructor( + private val userRepository: UserRepository +) : ViewModel() { + private val _loginState = MutableStateFlow(LoginState.Idle) + val loginState: StateFlow = _loginState + + private val _token = MutableLiveData(null) + val token: LiveData get() = _token + + fun postUserLogin( + context: Context, + body: UserLoginModel, + onSuccess: () -> Unit + ) { + _loginState.value = LoginState.Loading + viewModelScope.launch { + val result = userRepository.postUserLogin( + userLoginModel = body + ) + _loginState.value = result.fold( + onSuccess = { response -> + _token.value = response.token + onSuccess() + LoginState.Success(response) + }, + onFailure = { error -> + handleErrorToast( + exception = error, + is400Error = R.string.fail_to_login, + is403Error = R.string.fail_to_login_invalid_password, + context = context + ) + LoginState.Failure(error.message.orEmpty()) + } + ) + } + } + + private val _userName = MutableLiveData("") + val userName: LiveData get() = _userName + + private val _password = MutableLiveData("") + val password: LiveData get() = _password + + + fun setUserName(newName: String) { + _userName.value = newName + } + + fun setPassword(newPassword: String) { + _password.value = newPassword + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/feature/main/BottomNavigationBar.kt b/app/src/main/java/org/sopt/and/presentation/main/BottomNavigationBar.kt similarity index 98% rename from app/src/main/java/org/sopt/and/feature/main/BottomNavigationBar.kt rename to app/src/main/java/org/sopt/and/presentation/main/BottomNavigationBar.kt index 1d4c79a..e21c435 100644 --- a/app/src/main/java/org/sopt/and/feature/main/BottomNavigationBar.kt +++ b/app/src/main/java/org/sopt/and/presentation/main/BottomNavigationBar.kt @@ -1,4 +1,4 @@ -package org.sopt.and.feature.main +package org.sopt.and.presentation.main import androidx.compose.foundation.Image import androidx.compose.foundation.layout.height diff --git a/app/src/main/java/org/sopt/and/feature/main/MainActivity.kt b/app/src/main/java/org/sopt/and/presentation/main/MainActivity.kt similarity index 93% rename from app/src/main/java/org/sopt/and/feature/main/MainActivity.kt rename to app/src/main/java/org/sopt/and/presentation/main/MainActivity.kt index a600584..cf1b66e 100644 --- a/app/src/main/java/org/sopt/and/feature/main/MainActivity.kt +++ b/app/src/main/java/org/sopt/and/presentation/main/MainActivity.kt @@ -1,4 +1,4 @@ -package org.sopt.and.feature.main +package org.sopt.and.presentation.main import android.annotation.SuppressLint import android.os.Bundle @@ -9,9 +9,11 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Scaffold import androidx.compose.ui.Modifier import androidx.navigation.compose.rememberNavController +import dagger.hilt.android.AndroidEntryPoint import org.sopt.and.ui.theme.ANDANDROIDTheme import org.sopt.and.utils.currentRoute +@AndroidEntryPoint class MainActivity : ComponentActivity() { @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") diff --git a/app/src/main/java/org/sopt/and/feature/main/NavGraph.kt b/app/src/main/java/org/sopt/and/presentation/main/NavGraph.kt similarity index 71% rename from app/src/main/java/org/sopt/and/feature/main/NavGraph.kt rename to app/src/main/java/org/sopt/and/presentation/main/NavGraph.kt index a2ae220..dcafde9 100644 --- a/app/src/main/java/org/sopt/and/feature/main/NavGraph.kt +++ b/app/src/main/java/org/sopt/and/presentation/main/NavGraph.kt @@ -1,4 +1,4 @@ -package org.sopt.and.feature.main +package org.sopt.and.presentation.main import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -12,16 +12,11 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import org.sopt.and.R -import org.sopt.and.feature.home.HomeScreen -import org.sopt.and.feature.home.HomeViewModel -import org.sopt.and.feature.login.LoginScreen -import org.sopt.and.feature.login.LoginViewModel -import org.sopt.and.feature.mypage.MyScreen -import org.sopt.and.feature.mypage.MyViewModel -import org.sopt.and.feature.search.SearchScreen -import org.sopt.and.feature.search.SearchViewModel -import org.sopt.and.feature.signup.SignUpScreen -import org.sopt.and.feature.signup.SignUpViewModel +import org.sopt.and.presentation.home.HomeScreen +import org.sopt.and.presentation.login.LoginScreen +import org.sopt.and.presentation.mypage.MyScreen +import org.sopt.and.presentation.search.SearchScreen +import org.sopt.and.presentation.signup.SignUpScreen import org.sopt.and.utils.toast @Composable @@ -54,7 +49,6 @@ fun NavGraph( popUpTo(Routes.Home.screen) { inclusive = true } } }, - viewModel = LoginViewModel() ) } composable(Routes.SignUp.screen) { @@ -65,20 +59,18 @@ fun NavGraph( popUpTo(Routes.Login.screen) { inclusive = true } } }, - viewModel = SignUpViewModel() ) } composable(Routes.Home.screen) { isLoggedIn = true - HomeScreen(navController = navController, viewModel = HomeViewModel()) + HomeScreen(navController = navController) } composable(Routes.Search.screen) { - SearchScreen(navController = navController, viewModel = SearchViewModel()) + SearchScreen(navController = navController) } composable(Routes.My.screen) { MyScreen( navController = navController, - viewModel = MyViewModel() ) } } diff --git a/app/src/main/java/org/sopt/and/feature/main/Routes.kt b/app/src/main/java/org/sopt/and/presentation/main/Routes.kt similarity index 84% rename from app/src/main/java/org/sopt/and/feature/main/Routes.kt rename to app/src/main/java/org/sopt/and/presentation/main/Routes.kt index d235d93..ee8123e 100644 --- a/app/src/main/java/org/sopt/and/feature/main/Routes.kt +++ b/app/src/main/java/org/sopt/and/presentation/main/Routes.kt @@ -1,4 +1,4 @@ -package org.sopt.and.feature.main +package org.sopt.and.presentation.main sealed class Routes (val screen: String){ object SignUp: Routes("signup") diff --git a/app/src/main/java/org/sopt/and/feature/mypage/MyScreen.kt b/app/src/main/java/org/sopt/and/presentation/mypage/MyScreen.kt similarity index 83% rename from app/src/main/java/org/sopt/and/feature/mypage/MyScreen.kt rename to app/src/main/java/org/sopt/and/presentation/mypage/MyScreen.kt index 2b30da0..1d7ba00 100644 --- a/app/src/main/java/org/sopt/and/feature/mypage/MyScreen.kt +++ b/app/src/main/java/org/sopt/and/presentation/mypage/MyScreen.kt @@ -1,4 +1,4 @@ -package org.sopt.and.feature.mypage +package org.sopt.and.presentation.mypage import android.content.Context import androidx.compose.foundation.background @@ -10,30 +10,33 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController import org.sopt.and.R -import org.sopt.and.core.designsystem.component.EmptyBox -import org.sopt.and.core.designsystem.component.ProfileBox -import org.sopt.and.core.designsystem.component.TicketBox +import org.sopt.and.presentation.core.component.EmptyBox +import org.sopt.and.presentation.core.component.ProfileBox +import org.sopt.and.presentation.core.component.TicketBox import org.sopt.and.ui.theme.WavveTheme @Composable fun MyScreen( navController: NavController, - viewModel: MyViewModel = viewModel() + viewModel: MyViewModel = hiltViewModel() ) { val context = LocalContext.current val sharedPreferences = context.getSharedPreferences("token", Context.MODE_PRIVATE) val token = sharedPreferences.getString("loginToken", "").orEmpty() - viewModel.getUserHobby(token = token) + LaunchedEffect(Unit) { + viewModel.getUserHobby(token = token) + } val hobby by viewModel.hobby.observeAsState("") diff --git a/app/src/main/java/org/sopt/and/presentation/mypage/MyState.kt b/app/src/main/java/org/sopt/and/presentation/mypage/MyState.kt new file mode 100644 index 0000000..03dba23 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/mypage/MyState.kt @@ -0,0 +1,10 @@ +package org.sopt.and.presentation.mypage + +import org.sopt.and.domain.model.response.Hobby + +sealed class MyState { + data object Idle : MyState() + data object Loading : MyState() + data class Success(val result: Hobby) : MyState() + data class Failure(val message: String) : MyState() +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/mypage/MyViewModel.kt b/app/src/main/java/org/sopt/and/presentation/mypage/MyViewModel.kt new file mode 100644 index 0000000..86d36ee --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/mypage/MyViewModel.kt @@ -0,0 +1,40 @@ +package org.sopt.and.presentation.mypage + +import android.util.Log +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.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import org.sopt.and.domain.repository.UserRepository +import javax.inject.Inject + +@HiltViewModel +class MyViewModel @Inject constructor( + private val userRepository: UserRepository +) : ViewModel() { + private val _myState = MutableStateFlow(MyState.Idle) + val myState: StateFlow get() = _myState + + private val _hobby = MutableLiveData("") + val hobby: LiveData get() = _hobby + + fun getUserHobby(token: String) { + _myState.value = MyState.Loading + viewModelScope.launch { + val result = userRepository.getUserHobby(token = token) + _myState.value = result.fold( + onSuccess = { response -> + _hobby.value = response.hobby + MyState.Success(response) + }, + onFailure = { error -> + MyState.Failure(error.message.toString()) + } + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/feature/search/SearchScreen.kt b/app/src/main/java/org/sopt/and/presentation/search/SearchScreen.kt similarity index 88% rename from app/src/main/java/org/sopt/and/feature/search/SearchScreen.kt rename to app/src/main/java/org/sopt/and/presentation/search/SearchScreen.kt index 17179b5..29ceea7 100644 --- a/app/src/main/java/org/sopt/and/feature/search/SearchScreen.kt +++ b/app/src/main/java/org/sopt/and/presentation/search/SearchScreen.kt @@ -1,4 +1,4 @@ -package org.sopt.and.feature.search +package org.sopt.and.presentation.search import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -9,12 +9,12 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController import org.sopt.and.ui.theme.WavveTheme @Composable -fun SearchScreen(navController: NavController, viewModel: SearchViewModel = viewModel()) { +fun SearchScreen(navController: NavController, viewModel: SearchViewModel = hiltViewModel()) { Scaffold( modifier = Modifier.fillMaxSize(), ) { innerPadding -> diff --git a/app/src/main/java/org/sopt/and/presentation/search/SearchViewModel.kt b/app/src/main/java/org/sopt/and/presentation/search/SearchViewModel.kt new file mode 100644 index 0000000..d789ce7 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/search/SearchViewModel.kt @@ -0,0 +1,10 @@ +package org.sopt.and.presentation.search + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class SearchViewModel @Inject constructor( +) : ViewModel() { +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/feature/signup/SignUpScreen.kt b/app/src/main/java/org/sopt/and/presentation/signup/SignUpScreen.kt similarity index 96% rename from app/src/main/java/org/sopt/and/feature/signup/SignUpScreen.kt rename to app/src/main/java/org/sopt/and/presentation/signup/SignUpScreen.kt index a1da24a..eaf270c 100644 --- a/app/src/main/java/org/sopt/and/feature/signup/SignUpScreen.kt +++ b/app/src/main/java/org/sopt/and/presentation/signup/SignUpScreen.kt @@ -1,4 +1,4 @@ -package org.sopt.and.feature.signup +package org.sopt.and.presentation.signup import androidx.activity.compose.LocalOnBackPressedDispatcherOwner import androidx.compose.foundation.background @@ -46,13 +46,13 @@ import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController import org.sopt.and.R -import org.sopt.and.core.designsystem.component.AuthTextField -import org.sopt.and.core.designsystem.component.ErrorDialog -import org.sopt.and.core.designsystem.component.WavveSignUpButton -import org.sopt.and.data.remote.model.request.UserSignUpRequestDto +import org.sopt.and.domain.model.request.UserSignUpModel +import org.sopt.and.presentation.core.component.AuthTextField +import org.sopt.and.presentation.core.component.ErrorDialog +import org.sopt.and.presentation.core.component.WavveSignUpButton import org.sopt.and.ui.theme.WavveTheme import org.sopt.and.utils.noRippleClickable @@ -61,7 +61,7 @@ import org.sopt.and.utils.noRippleClickable fun SignUpScreen( navController: NavController, onSignUpSuccess: (String, String) -> Unit, - viewModel: SignUpViewModel = viewModel() + viewModel: SignUpViewModel = hiltViewModel() ) { val focusManager = LocalFocusManager.current val dispatcher = LocalOnBackPressedDispatcherOwner.current!!.onBackPressedDispatcher @@ -243,7 +243,7 @@ fun SignUpScreen( onSuccess = { userName, password, hobby -> viewModel.postUserSignUp( context = context, - body = UserSignUpRequestDto( + body = UserSignUpModel( username = userName, password = password, hobby = hobby diff --git a/app/src/main/java/org/sopt/and/presentation/signup/SignUpState.kt b/app/src/main/java/org/sopt/and/presentation/signup/SignUpState.kt new file mode 100644 index 0000000..d4d0863 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/signup/SignUpState.kt @@ -0,0 +1,10 @@ +package org.sopt.and.presentation.signup + +import org.sopt.and.domain.model.response.UserNumber + +sealed class SignUpState { + data object Idle : SignUpState() + data object Loading : SignUpState() + data class Success(val result: UserNumber) : SignUpState() + data class Failure(val message: String) : SignUpState() +} diff --git a/app/src/main/java/org/sopt/and/feature/signup/SignUpViewModel.kt b/app/src/main/java/org/sopt/and/presentation/signup/SignUpViewModel.kt similarity index 66% rename from app/src/main/java/org/sopt/and/feature/signup/SignUpViewModel.kt rename to app/src/main/java/org/sopt/and/presentation/signup/SignUpViewModel.kt index c46883f..38e3020 100644 --- a/app/src/main/java/org/sopt/and/feature/signup/SignUpViewModel.kt +++ b/app/src/main/java/org/sopt/and/presentation/signup/SignUpViewModel.kt @@ -1,7 +1,6 @@ -package org.sopt.and.feature.signup +package org.sopt.and.presentation.signup import android.content.Context -import android.util.Log import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -10,47 +9,42 @@ 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.sopt.and.R -import org.sopt.and.data.ServicePool -import org.sopt.and.data.remote.model.request.UserSignUpRequestDto -import org.sopt.and.data.remote.model.response.UserSignUpResponseDto +import org.sopt.and.domain.model.request.UserSignUpModel +import org.sopt.and.domain.repository.UserRepository import org.sopt.and.utils.AuthKey.PASSWORD_PATTERN -import org.sopt.and.utils.toast -import retrofit2.HttpException -import java.io.IOException - -class SignUpViewModel : ViewModel() { - private val userService by lazy { ServicePool.userService } - - private val _userState = mutableStateOf(null) - val userState: State get() = _userState - - fun postUserSignUp(context: Context, body: UserSignUpRequestDto) { +import org.sopt.and.utils.handleErrorToast +import javax.inject.Inject + +@HiltViewModel +class SignUpViewModel @Inject constructor( + private val userRepository: UserRepository +) : ViewModel() { + private val _signUpState = mutableStateOf(SignUpState.Idle) + val signUpState: State get() = _signUpState + + fun postUserSignUp(context: Context, body: UserSignUpModel) { + _signUpState.value = SignUpState.Loading viewModelScope.launch { - runCatching { - userService.postUserSignUp(body = body) - }.onSuccess { response -> - _userState.value = response.result - }.onFailure { error -> - when (error) { - is HttpException -> { - when (error.code()) { - 400 -> context.toast(context.getString(R.string.fail_to_signup_maximum_length)) - 409 -> context.toast(context.getString(R.string.fail_to_signup_duplicate_name)) - } - } - - is IOException -> { - context.toast(context.getString(R.string.fail_to_network)) - } - - else -> { - context.toast(context.getString(R.string.fail_to_signup)) - } + val result = userRepository.postUserSignUp( + userSignUpModel = body + ) + _signUpState.value = result.fold( + onSuccess = { response -> + SignUpState.Success(response) + }, + onFailure = { error -> + handleErrorToast( + exception = error, + is400Error = R.string.fail_to_signup_maximum_length, + is409Error = R.string.fail_to_signup_duplicate_name, + context = context + ) + SignUpState.Failure(error.message.orEmpty()) } - Log.e("postUserSignUpError", error.toString()) - } + ) } } diff --git a/app/src/main/java/org/sopt/and/utils/ErrorHandler.kt b/app/src/main/java/org/sopt/and/utils/ErrorHandler.kt new file mode 100644 index 0000000..551501a --- /dev/null +++ b/app/src/main/java/org/sopt/and/utils/ErrorHandler.kt @@ -0,0 +1,31 @@ +package org.sopt.and.utils + +import android.content.Context +import org.sopt.and.R +import retrofit2.HttpException +import java.io.IOException + +fun handleErrorToast( + exception: Throwable?, + is400Error: Int, + context: Context, + is403Error: Int = R.string.fail_to_network, + is409Error: Int = R.string.fail_to_network, +) { + return when (exception) { + is HttpException -> when (exception.code()) { + 400 -> context.toast(context.getString(is400Error)) + 403 -> context.toast(context.getString(is403Error)) + 409 -> context.toast(context.getString(is409Error)) + else -> context.toast(context.getString(R.string.fail_to_network)) + } + + is IOException -> { + context.toast(context.getString(R.string.fail_to_network)) + } + + else -> { + context.toast(context.getString(R.string.fail_to_login)) + } + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2737244..a5697db 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,6 +18,9 @@ okhttp = "4.11.0" retrofit = "2.9.0" retrofitKotlinSerializationConverter = "1.0.0" kotlinxSerializationJson = "1.6.3" +# hilt +androidx-hilt-navigation-compose = "1.2.0" +hilt = "2.51.1" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -46,9 +49,14 @@ okhttp-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-i retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } retrofit-kotlin-serialization-converter = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version.ref = "retrofitKotlinSerializationConverter" } kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } +# hilt +hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } +hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" } +hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "androidx-hilt-navigation-compose" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } \ No newline at end of file