diff --git a/app/build.gradle b/app/build.gradle index e04dd53..a35f3c7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -28,11 +28,8 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" buildConfigField "String", "AUTH_BASE_URL", properties["auth.base.url"] buildConfigField "String", "REQRES_BASE_URL", properties["reqres.base.url"] + buildConfigField "String", "MUSIC_BASE_URL", properties["music.base.url"] } - - - - buildTypes { release { minifyEnabled false @@ -80,7 +77,7 @@ dependencies { implementation("com.squareup.okhttp3:logging-interceptor") //Glide - implementation 'com.github.bumptech.glide:glide:4.9.0' + implementation 'com.github.bumptech.glide:glide:4.12.0' //Fliper 디버거 debugImplementation 'com.facebook.flipper:flipper:0.174.0' @@ -100,4 +97,6 @@ dependencies { implementation "com.google.dagger:hilt-android:$hilt_version" kapt "com.google.dagger:hilt-android-compiler:$hilt_version" + //coil + implementation 'io.coil-kt:coil:2.2.2' } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b0197ba..a7c63e8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,10 @@ - + + + + - + android:exported="false"> - - - - + android:name=".presentation.sign.signup.SignupActivity" + android:exported="false"> + + - + android:name=".presentation.sign.signin.SignInActivity" + android:exported="true"> + + + + diff --git a/app/src/main/java/org/sopt/sample/application/ApplicationClass.kt b/app/src/main/java/org/sopt/sample/application/ApplicationClass.kt index fe4a8c1..f33f37c 100644 --- a/app/src/main/java/org/sopt/sample/application/ApplicationClass.kt +++ b/app/src/main/java/org/sopt/sample/application/ApplicationClass.kt @@ -11,20 +11,19 @@ import com.facebook.soloader.SoLoader import dagger.hilt.android.HiltAndroidApp import org.sopt.sample.BuildConfig import timber.log.Timber + @HiltAndroidApp class ApplicationClass : Application() { - companion object{ - private lateinit var applicationClass: ApplicationClass - fun getInstance(): ApplicationClass = applicationClass - val networkFlipperPlugin = NetworkFlipperPlugin() - } + override fun onCreate() { super.onCreate() + applicationClass = this initFlipper() - if(BuildConfig.DEBUG){ + if (BuildConfig.DEBUG) { Timber.plant(Timber.DebugTree()) } + } @@ -32,11 +31,22 @@ class ApplicationClass : Application() { SoLoader.init(this, false) if (BuildConfig.DEBUG && FlipperUtils.shouldEnableFlipper(this)) { AndroidFlipperClient.getInstance(this).apply { - addPlugin(InspectorFlipperPlugin(this@ApplicationClass, DescriptorMapping.withDefaults())) + addPlugin( + InspectorFlipperPlugin( + this@ApplicationClass, + DescriptorMapping.withDefaults() + ) + ) addPlugin(networkFlipperPlugin) addPlugin(LeakCanary2FlipperPlugin()) //-> 디버그에서 메모리 릭 잡아주는 친구 // addPlugin(SharedPreferencesFlipperPlugin(app, "SOPT_DATA")) -> 해당 키를 가진 SharedPreference 내부 데이터를 볼 수 있는 친구 }.start() } } + + companion object { + private lateinit var applicationClass: ApplicationClass + fun getInstance() = applicationClass + val networkFlipperPlugin = NetworkFlipperPlugin() + } } \ No newline at end of file diff --git a/app/src/main/java/org/sopt/sample/base/BindingDialog.kt b/app/src/main/java/org/sopt/sample/base/BindingDialog.kt new file mode 100644 index 0000000..1495cc2 --- /dev/null +++ b/app/src/main/java/org/sopt/sample/base/BindingDialog.kt @@ -0,0 +1,31 @@ +package org.sopt.sample.base + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.LayoutRes +import androidx.databinding.DataBindingUtil +import androidx.databinding.ViewDataBinding +import androidx.fragment.app.DialogFragment + +abstract class BindingDialog(@LayoutRes private val layoutResId: Int) : + DialogFragment() { + private var _binding: B? = null + protected val binding + get() = requireNotNull(_binding!!) { "${this::class.java.simpleName} error" } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = DataBindingUtil.inflate(inflater, layoutResId, container, false) + return binding.root + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/sample/data/api/ApiClient.kt b/app/src/main/java/org/sopt/sample/data/api/ApiClient.kt index 6f4765f..102e3bd 100644 --- a/app/src/main/java/org/sopt/sample/data/api/ApiClient.kt +++ b/app/src/main/java/org/sopt/sample/data/api/ApiClient.kt @@ -11,27 +11,31 @@ import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import org.sopt.sample.BuildConfig import org.sopt.sample.application.ApplicationClass +import org.sopt.sample.data.api.ApiClient.MUSIC_BASE_URL import retrofit2.Retrofit object ApiClient { private const val AUTH_BASE_URL = BuildConfig.AUTH_BASE_URL private const val REQRES_BASE_URL = BuildConfig.REQRES_BASE_URL + private const val MUSIC_BASE_URL = BuildConfig.MUSIC_BASE_URL private var authRetrofit: Retrofit? = null private var reqresRetrofit: Retrofit? = null - + private var musicRetrofit: Retrofit? = null + private val logger = HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + } + private val client by lazy { + OkHttpClient.Builder() + .addInterceptor(logger) + .addNetworkInterceptor(FlipperOkhttpInterceptor(ApplicationClass.networkFlipperPlugin)) + .build() + } //AUTH API @OptIn(ExperimentalSerializationApi::class, InternalCoroutinesApi::class) //동기화 처리를 통해 멀티쓰레드 환경에서 인스턴스가 2개 생성되는 것을 방지 fun getRetrofitForAuth(): Retrofit? { synchronized(this) { if (authRetrofit == null) { - val logger = HttpLoggingInterceptor().apply { - level = HttpLoggingInterceptor.Level.BODY - } - val client = OkHttpClient.Builder() - .addInterceptor(logger) - .addNetworkInterceptor(FlipperOkhttpInterceptor(ApplicationClass.networkFlipperPlugin)) - .build() authRetrofit = Retrofit.Builder() .baseUrl(AUTH_BASE_URL) .client(client) @@ -41,20 +45,11 @@ object ApiClient { return authRetrofit } } - //UserList API @OptIn(ExperimentalSerializationApi::class, InternalCoroutinesApi::class) - @Synchronized fun getRetrofitForUserList(): Retrofit? { synchronized(this) { if (reqresRetrofit == null) { - val logger = HttpLoggingInterceptor().apply { - level = HttpLoggingInterceptor.Level.BODY - } - val client = OkHttpClient.Builder() - .addInterceptor(logger) - .addNetworkInterceptor(FlipperOkhttpInterceptor(ApplicationClass.networkFlipperPlugin)) - .build() reqresRetrofit = Retrofit.Builder() .baseUrl(REQRES_BASE_URL) .client(client) @@ -64,4 +59,18 @@ object ApiClient { return reqresRetrofit } } + //Music API + @OptIn(ExperimentalSerializationApi::class, InternalCoroutinesApi::class) + fun getRetrofitForMusicList():Retrofit?{ + synchronized(this) { + if (musicRetrofit == null) { + musicRetrofit = Retrofit.Builder() + .baseUrl(MUSIC_BASE_URL) + .client(client) + .addConverterFactory(Json.asConverterFactory("application/json".toMediaType())) + .build() + } + return musicRetrofit + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/sopt/sample/data/auth/Service/AuthService.kt b/app/src/main/java/org/sopt/sample/data/api/AuthService.kt similarity index 58% rename from app/src/main/java/org/sopt/sample/data/auth/Service/AuthService.kt rename to app/src/main/java/org/sopt/sample/data/api/AuthService.kt index 6b0d04a..6276de3 100644 --- a/app/src/main/java/org/sopt/sample/data/auth/Service/AuthService.kt +++ b/app/src/main/java/org/sopt/sample/data/api/AuthService.kt @@ -1,9 +1,9 @@ -package org.sopt.sample.data.auth.service +package org.sopt.sample.data.api -import org.sopt.sample.data.auth.model.SignInRequest -import org.sopt.sample.data.auth.model.SignInResponse -import org.sopt.sample.data.auth.model.SignUpRequest -import org.sopt.sample.data.auth.model.SignUpResponse +import org.sopt.sample.data.model.SignInRequest +import org.sopt.sample.data.model.SignInResponse +import org.sopt.sample.data.model.SignUpRequest +import org.sopt.sample.data.model.SignUpResponse import retrofit2.http.Body import retrofit2.http.POST diff --git a/app/src/main/java/org/sopt/sample/data/home/service/HomeService.kt b/app/src/main/java/org/sopt/sample/data/api/HomeService.kt similarity index 70% rename from app/src/main/java/org/sopt/sample/data/home/service/HomeService.kt rename to app/src/main/java/org/sopt/sample/data/api/HomeService.kt index a04c723..11091fb 100644 --- a/app/src/main/java/org/sopt/sample/data/home/service/HomeService.kt +++ b/app/src/main/java/org/sopt/sample/data/api/HomeService.kt @@ -1,6 +1,6 @@ -package org.sopt.sample.data.home.service +package org.sopt.sample.data.api -import org.sopt.sample.data.home.model.UserListResponse +import org.sopt.sample.data.model.UserListResponse import retrofit2.http.GET import retrofit2.http.Query diff --git a/app/src/main/java/org/sopt/sample/data/api/MusicService.kt b/app/src/main/java/org/sopt/sample/data/api/MusicService.kt new file mode 100644 index 0000000..be6bf10 --- /dev/null +++ b/app/src/main/java/org/sopt/sample/data/api/MusicService.kt @@ -0,0 +1,24 @@ +package org.sopt.sample.data.api + +import okhttp3.MultipartBody +import okhttp3.RequestBody +import org.sopt.sample.data.model.MusicResponse +import org.sopt.sample.data.model.ResponseUploadMusic +import retrofit2.http.GET +import retrofit2.http.Multipart +import retrofit2.http.POST +import retrofit2.http.Part + +interface MusicService { + // 음악 리스트 가져오기 + @GET("/music/list") + suspend fun fetchMusicList(): MusicResponse + + //음악 생성하기 + @Multipart + @POST("/music") + suspend fun uploadMusic( + @Part("request") request: RequestBody, + @Part image: MultipartBody.Part + ):ResponseUploadMusic +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/sample/data/auth/repository/AuthRepository.kt b/app/src/main/java/org/sopt/sample/data/auth/repository/AuthRepository.kt deleted file mode 100644 index 1d47961..0000000 --- a/app/src/main/java/org/sopt/sample/data/auth/repository/AuthRepository.kt +++ /dev/null @@ -1,16 +0,0 @@ -package org.sopt.sample.data.auth.repository - -import org.sopt.sample.data.auth.model.SignInRequest -import org.sopt.sample.data.auth.model.SignInResponse -import org.sopt.sample.data.auth.model.SignUpRequest -import org.sopt.sample.data.auth.model.SignUpResponse -import org.sopt.sample.data.auth.source.AuthDataSource - -class AuthRepository(private val authDataSource: AuthDataSource) { - suspend fun signIn(signInRequest: SignInRequest):SignInResponse{ - return authDataSource.signIn(signInRequest) - } - suspend fun signUp(signUpRequest: SignUpRequest):SignUpResponse{ - return authDataSource.signUp(signUpRequest) - } -} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/sample/data/auth/source/AuthDataSource.kt b/app/src/main/java/org/sopt/sample/data/auth/source/AuthDataSource.kt deleted file mode 100644 index 6c86ebd..0000000 --- a/app/src/main/java/org/sopt/sample/data/auth/source/AuthDataSource.kt +++ /dev/null @@ -1,19 +0,0 @@ -package org.sopt.sample.data.auth.source - -import org.sopt.sample.data.auth.service.AuthService -import org.sopt.sample.data.auth.model.SignInRequest -import org.sopt.sample.data.auth.model.SignInResponse -import org.sopt.sample.data.auth.model.SignUpRequest -import org.sopt.sample.data.auth.model.SignUpResponse - -interface AuthDataSource { - suspend fun signIn(signInRequest: SignInRequest):SignInResponse - suspend fun signUp(signUpRequest: SignUpRequest):SignUpResponse -} -class AuthDataSourceImpl(private val authService: AuthService) : AuthDataSource { - override suspend fun signIn(signInRequest: SignInRequest): SignInResponse = - authService.signIn(signInRequest) - - override suspend fun signUp(signUpRequest: SignUpRequest): SignUpResponse = - authService.signUp(signUpRequest) -} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/sample/data/home/repository/HomeRepository.kt b/app/src/main/java/org/sopt/sample/data/home/repository/HomeRepository.kt deleted file mode 100644 index fce33dd..0000000 --- a/app/src/main/java/org/sopt/sample/data/home/repository/HomeRepository.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.sopt.sample.data.home.repository - -import org.sopt.sample.data.home.model.UserListResponse -import org.sopt.sample.data.home.source.HomeDataSource - -class HomeRepository(private val homeDataSource: HomeDataSource) { - suspend fun loadUser(page: Int): UserListResponse = homeDataSource.loadUser(page) - -} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/sample/data/home/source/HomeDataSource.kt b/app/src/main/java/org/sopt/sample/data/home/source/HomeDataSource.kt deleted file mode 100644 index 89a5b7e..0000000 --- a/app/src/main/java/org/sopt/sample/data/home/source/HomeDataSource.kt +++ /dev/null @@ -1,7 +0,0 @@ -package org.sopt.sample.data.home.source - -import org.sopt.sample.data.home.model.UserListResponse - -interface HomeDataSource { - suspend fun loadUser(page:Int): UserListResponse -} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/sample/data/home/source/HomeDataSourceImpl.kt b/app/src/main/java/org/sopt/sample/data/home/source/HomeDataSourceImpl.kt deleted file mode 100644 index 2a259bb..0000000 --- a/app/src/main/java/org/sopt/sample/data/home/source/HomeDataSourceImpl.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.sopt.sample.data.home.source - -import org.sopt.sample.data.home.service.HomeService -import org.sopt.sample.data.home.model.UserListResponse - -class HomeDataSourceImpl(private val homeService: HomeService) : HomeDataSource { - override suspend fun loadUser(page: Int): UserListResponse = homeService.loadUserList(page) - -} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/sample/data/auth/model/Auth.kt b/app/src/main/java/org/sopt/sample/data/model/Auth.kt similarity index 96% rename from app/src/main/java/org/sopt/sample/data/auth/model/Auth.kt rename to app/src/main/java/org/sopt/sample/data/model/Auth.kt index abf1125..969c6b1 100644 --- a/app/src/main/java/org/sopt/sample/data/auth/model/Auth.kt +++ b/app/src/main/java/org/sopt/sample/data/model/Auth.kt @@ -1,4 +1,4 @@ -package org.sopt.sample.data.auth.model +package org.sopt.sample.data.model import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/app/src/main/java/org/sopt/sample/data/home/model/Home.kt b/app/src/main/java/org/sopt/sample/data/model/Home.kt similarity index 95% rename from app/src/main/java/org/sopt/sample/data/home/model/Home.kt rename to app/src/main/java/org/sopt/sample/data/model/Home.kt index eaf5671..1782b46 100644 --- a/app/src/main/java/org/sopt/sample/data/home/model/Home.kt +++ b/app/src/main/java/org/sopt/sample/data/model/Home.kt @@ -1,4 +1,4 @@ -package org.sopt.sample.data.home.model +package org.sopt.sample.data.model import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/app/src/main/java/org/sopt/sample/data/model/Music.kt b/app/src/main/java/org/sopt/sample/data/model/Music.kt new file mode 100644 index 0000000..c75af9f --- /dev/null +++ b/app/src/main/java/org/sopt/sample/data/model/Music.kt @@ -0,0 +1,28 @@ +package org.sopt.sample.data.model + +import kotlinx.serialization.Serializable + +@Serializable +data class MusicResponse( + val data: List, + val message: String, + val statusCode: Int, + val success: Boolean +) + +@Serializable +data class ResponseUploadMusic( + val data: Music, + val message: String, + val statusCode: Int, + val success: Boolean +) + +@Serializable +data class Music( + val id: Int, + val image: String, + val singer: String, + val title: String +) + diff --git a/app/src/main/java/org/sopt/sample/data/repository/AuthRepositoryImpl.kt b/app/src/main/java/org/sopt/sample/data/repository/AuthRepositoryImpl.kt new file mode 100644 index 0000000..a7f758d --- /dev/null +++ b/app/src/main/java/org/sopt/sample/data/repository/AuthRepositoryImpl.kt @@ -0,0 +1,17 @@ +package org.sopt.sample.data.repository + +import org.sopt.sample.data.model.SignInRequest +import org.sopt.sample.data.model.SignInResponse +import org.sopt.sample.data.model.SignUpRequest +import org.sopt.sample.data.model.SignUpResponse +import org.sopt.sample.data.source.remote.AuthDataSource +import org.sopt.sample.domain.AuthRepository + +class AuthRepositoryImpl(private val authDataSource: AuthDataSource):AuthRepository { + override suspend fun signIn(signInRequest: SignInRequest): SignInResponse { + return authDataSource.signIn(signInRequest) + } + override suspend fun signUp(signUpRequest: SignUpRequest): SignUpResponse { + return authDataSource.signUp(signUpRequest) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/sample/data/repository/HomeRepositoryImpl.kt b/app/src/main/java/org/sopt/sample/data/repository/HomeRepositoryImpl.kt new file mode 100644 index 0000000..1fd7105 --- /dev/null +++ b/app/src/main/java/org/sopt/sample/data/repository/HomeRepositoryImpl.kt @@ -0,0 +1,9 @@ +package org.sopt.sample.data.repository + +import org.sopt.sample.data.model.UserListResponse +import org.sopt.sample.data.source.remote.HomeDataSource +import org.sopt.sample.domain.HomeRepository + +class HomeRepositoryImpl(private val homeDataSource: HomeDataSource) : HomeRepository { + override suspend fun loadUser(page: Int): UserListResponse = homeDataSource.loadUser(page) +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/sample/data/repository/MusicRepositoryImpl.kt b/app/src/main/java/org/sopt/sample/data/repository/MusicRepositoryImpl.kt new file mode 100644 index 0000000..191d993 --- /dev/null +++ b/app/src/main/java/org/sopt/sample/data/repository/MusicRepositoryImpl.kt @@ -0,0 +1,18 @@ +package org.sopt.sample.data.repository + +import android.util.Log +import okhttp3.MultipartBody +import okhttp3.RequestBody +import org.sopt.sample.data.model.MusicResponse +import org.sopt.sample.data.model.ResponseUploadMusic +import org.sopt.sample.data.source.remote.MusicDataSource +import org.sopt.sample.domain.MusicRepository + +class MusicRepositoryImpl(private val musicDataSource: MusicDataSource) : MusicRepository { + override suspend fun fetchMusicList(): MusicResponse = musicDataSource.fetchMusicList() + override suspend fun uploadMusic( + musicRequest: RequestBody, + image: MultipartBody.Part + ): ResponseUploadMusic { + return musicDataSource.uploadMusic(musicRequest, image)} +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/sample/data/source/remote/AuthDataSource.kt b/app/src/main/java/org/sopt/sample/data/source/remote/AuthDataSource.kt new file mode 100644 index 0000000..2d4888c --- /dev/null +++ b/app/src/main/java/org/sopt/sample/data/source/remote/AuthDataSource.kt @@ -0,0 +1,15 @@ +package org.sopt.sample.data.source.remote + +import org.sopt.sample.data.api.AuthService +import org.sopt.sample.data.model.SignInRequest +import org.sopt.sample.data.model.SignInResponse +import org.sopt.sample.data.model.SignUpRequest +import org.sopt.sample.data.model.SignUpResponse + +class AuthDataSource(private val authService: AuthService) { + suspend fun signIn(signInRequest: SignInRequest): SignInResponse = + authService.signIn(signInRequest) + + suspend fun signUp(signUpRequest: SignUpRequest): SignUpResponse = + authService.signUp(signUpRequest) +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/sample/data/source/remote/HomeDataSource.kt b/app/src/main/java/org/sopt/sample/data/source/remote/HomeDataSource.kt new file mode 100644 index 0000000..b9e5810 --- /dev/null +++ b/app/src/main/java/org/sopt/sample/data/source/remote/HomeDataSource.kt @@ -0,0 +1,9 @@ +package org.sopt.sample.data.source.remote + +import org.sopt.sample.data.api.HomeService +import org.sopt.sample.data.model.UserListResponse + +class HomeDataSource(private val homeService: HomeService) { + suspend fun loadUser(page: Int): UserListResponse = + homeService.loadUserList(page) +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/sample/data/source/remote/MusicDataSource.kt b/app/src/main/java/org/sopt/sample/data/source/remote/MusicDataSource.kt new file mode 100644 index 0000000..2f4cc56 --- /dev/null +++ b/app/src/main/java/org/sopt/sample/data/source/remote/MusicDataSource.kt @@ -0,0 +1,18 @@ +package org.sopt.sample.data.source.remote + +import okhttp3.MultipartBody +import okhttp3.RequestBody +import org.sopt.sample.data.api.MusicService +import org.sopt.sample.data.model.MusicResponse +import org.sopt.sample.data.model.ResponseUploadMusic + +class MusicDataSource(private val musicService: MusicService) { + suspend fun fetchMusicList(): MusicResponse = musicService.fetchMusicList() + + suspend fun uploadMusic( + musicRequest: RequestBody, + image: MultipartBody.Part + ): ResponseUploadMusic { + return musicService.uploadMusic(musicRequest, image) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/sample/di/ApplicationModule.kt b/app/src/main/java/org/sopt/sample/di/ApplicationModule.kt new file mode 100644 index 0000000..c2ffd6c --- /dev/null +++ b/app/src/main/java/org/sopt/sample/di/ApplicationModule.kt @@ -0,0 +1,20 @@ +package org.sopt.sample.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import org.sopt.sample.application.ApplicationClass +import javax.inject.Singleton + + +@Module +@InstallIn(SingletonComponent::class) + +object ApplicationModule { + @Singleton + @Provides + fun provideApplication(): ApplicationClass { + return ApplicationClass.getInstance() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/sample/di/RepositoryModule.kt b/app/src/main/java/org/sopt/sample/di/RepositoryModule.kt index 300af1c..9effff7 100644 --- a/app/src/main/java/org/sopt/sample/di/RepositoryModule.kt +++ b/app/src/main/java/org/sopt/sample/di/RepositoryModule.kt @@ -5,12 +5,18 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import org.sopt.sample.data.api.ApiClient -import org.sopt.sample.data.auth.repository.AuthRepository -import org.sopt.sample.data.auth.service.AuthService -import org.sopt.sample.data.auth.source.AuthDataSourceImpl -import org.sopt.sample.data.home.repository.HomeRepository -import org.sopt.sample.data.home.service.HomeService -import org.sopt.sample.data.home.source.HomeDataSourceImpl +import org.sopt.sample.data.api.AuthService +import org.sopt.sample.data.api.HomeService +import org.sopt.sample.data.api.MusicService +import org.sopt.sample.data.repository.AuthRepositoryImpl +import org.sopt.sample.data.repository.HomeRepositoryImpl +import org.sopt.sample.data.repository.MusicRepositoryImpl +import org.sopt.sample.data.source.remote.AuthDataSource +import org.sopt.sample.data.source.remote.HomeDataSource +import org.sopt.sample.data.source.remote.MusicDataSource +import org.sopt.sample.domain.AuthRepository +import org.sopt.sample.domain.HomeRepository +import org.sopt.sample.domain.MusicRepository import javax.inject.Singleton @Module @@ -19,8 +25,8 @@ object RepositoryModule { @Singleton @Provides fun provideHomeRepository(): HomeRepository { - return HomeRepository( - HomeDataSourceImpl( + return HomeRepositoryImpl( + HomeDataSource( ApiClient.getRetrofitForUserList()!!.create(HomeService::class.java) ) ) @@ -29,10 +35,20 @@ object RepositoryModule { @Singleton @Provides fun provideSignRepository(): AuthRepository { - return AuthRepository( - AuthDataSourceImpl( + return AuthRepositoryImpl( + AuthDataSource( ApiClient.getRetrofitForAuth()!!.create(AuthService::class.java) ) ) } + + @Singleton + @Provides + fun provideMusicRepository(): MusicRepository { + return MusicRepositoryImpl( + MusicDataSource( + ApiClient.getRetrofitForMusicList()!!.create(MusicService::class.java) + ) + ) + } } \ No newline at end of file diff --git a/app/src/main/java/org/sopt/sample/domain/AuthRepository.kt b/app/src/main/java/org/sopt/sample/domain/AuthRepository.kt new file mode 100644 index 0000000..454d4ec --- /dev/null +++ b/app/src/main/java/org/sopt/sample/domain/AuthRepository.kt @@ -0,0 +1,11 @@ +package org.sopt.sample.domain + +import org.sopt.sample.data.model.SignInRequest +import org.sopt.sample.data.model.SignInResponse +import org.sopt.sample.data.model.SignUpRequest +import org.sopt.sample.data.model.SignUpResponse + +interface AuthRepository { + suspend fun signIn(signInRequest: SignInRequest): SignInResponse + suspend fun signUp(signUpRequest: SignUpRequest): SignUpResponse +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/sample/domain/HomeRepository.kt b/app/src/main/java/org/sopt/sample/domain/HomeRepository.kt new file mode 100644 index 0000000..7d0a127 --- /dev/null +++ b/app/src/main/java/org/sopt/sample/domain/HomeRepository.kt @@ -0,0 +1,7 @@ +package org.sopt.sample.domain + +import org.sopt.sample.data.model.UserListResponse + +interface HomeRepository { + suspend fun loadUser(page: Int): UserListResponse +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/sample/domain/MusicRepository.kt b/app/src/main/java/org/sopt/sample/domain/MusicRepository.kt new file mode 100644 index 0000000..dc5a053 --- /dev/null +++ b/app/src/main/java/org/sopt/sample/domain/MusicRepository.kt @@ -0,0 +1,14 @@ +package org.sopt.sample.domain + +import okhttp3.MultipartBody +import okhttp3.RequestBody +import org.sopt.sample.data.model.MusicResponse +import org.sopt.sample.data.model.ResponseUploadMusic + +interface MusicRepository { + suspend fun fetchMusicList(): MusicResponse + suspend fun uploadMusic( + musicRequest: RequestBody, + image: MultipartBody.Part + ): ResponseUploadMusic +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/sample/presentation/MainActivity.kt b/app/src/main/java/org/sopt/sample/presentation/MainActivity.kt index 7b8400b..f9e2fff 100644 --- a/app/src/main/java/org/sopt/sample/presentation/MainActivity.kt +++ b/app/src/main/java/org/sopt/sample/presentation/MainActivity.kt @@ -10,6 +10,7 @@ import org.sopt.sample.R import org.sopt.sample.base.BindingActivity import org.sopt.sample.databinding.ActivityMainBinding import org.sopt.sample.presentation.home.HomeFragment +import org.sopt.sample.presentation.music.MusicFragment import org.sopt.sample.presentation.search.SearchFragment @AndroidEntryPoint @@ -46,6 +47,12 @@ class MainActivity : BindingActivity(R.layout.activity_main HomeFragment::class.java.simpleName ) } + R.id.btm_music_menu->supportFragmentManager.commit { + replace( + R.id.main_fragment_frame, + MusicFragment::class.java.simpleName + ) + } R.id.btm_search_menu -> supportFragmentManager.commit { replace( R.id.main_fragment_frame, diff --git a/app/src/main/java/org/sopt/sample/presentation/home/HomeFragment.kt b/app/src/main/java/org/sopt/sample/presentation/home/HomeFragment.kt index 750a365..ddc6ca2 100644 --- a/app/src/main/java/org/sopt/sample/presentation/home/HomeFragment.kt +++ b/app/src/main/java/org/sopt/sample/presentation/home/HomeFragment.kt @@ -1,6 +1,7 @@ package org.sopt.sample.presentation.home import android.os.Bundle +import android.util.Log import android.view.View import androidx.fragment.app.viewModels import dagger.hilt.android.AndroidEntryPoint @@ -8,11 +9,12 @@ import org.sopt.sample.R import org.sopt.sample.base.BindingFragment import org.sopt.sample.databinding.FragmentHomeBinding import org.sopt.sample.presentation.home.adapter.HomeUserListAdapter +import org.sopt.sample.presentation.state.UiState import org.sopt.sample.util.RecyclerDecorationHeight +import timber.log.Timber @AndroidEntryPoint class HomeFragment : BindingFragment(R.layout.fragment_home) { -// private val viewModel: HomeViewModel by viewModels { ViewModelFactory() } private val viewModel: HomeViewModel by viewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -27,16 +29,18 @@ class HomeFragment : BindingFragment(R.layout.fragment_home //load 성공 여부 observe 후 adapter로 userList 전달 private fun addObserver(adapter: HomeUserListAdapter) { - viewModel.loadUserSuccess.observe(viewLifecycleOwner) { - if (it) { - viewModel.userList.value?.let { userList -> adapter.submitUserList(userList) } - } - } - viewModel.isLoading.observe(viewLifecycleOwner) { - if (it) { - showSampleData() - } else { - hideSampleData() + viewModel.homeState.observe(viewLifecycleOwner) { + when (it) { + is UiState.Loading -> { + showSampleData() + } + is UiState.Success -> { + viewModel.userList.value?.let { userList -> adapter.submitUserList(userList) } + hideSampleData() + } + else -> { + hideSampleData() + } } } diff --git a/app/src/main/java/org/sopt/sample/presentation/home/HomeViewModel.kt b/app/src/main/java/org/sopt/sample/presentation/home/HomeViewModel.kt index e3dfe0d..91ac4ca 100644 --- a/app/src/main/java/org/sopt/sample/presentation/home/HomeViewModel.kt +++ b/app/src/main/java/org/sopt/sample/presentation/home/HomeViewModel.kt @@ -7,8 +7,9 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import org.sopt.sample.data.home.model.User -import org.sopt.sample.data.home.repository.HomeRepository +import org.sopt.sample.data.model.User +import org.sopt.sample.domain.HomeRepository +import org.sopt.sample.presentation.state.UiState import org.sopt.sample.util.extensions.toUserList import javax.inject.Inject @@ -16,34 +17,23 @@ import javax.inject.Inject class HomeViewModel @Inject constructor(private val homeRepository: HomeRepository) : ViewModel() { val userList = MutableLiveData>() - private val _loadUserSuccess = MutableLiveData() - val loadUserSuccess: LiveData - get() = _loadUserSuccess - - //로딩 시작 시, isLoading = true - //로딩이 끝나면, isLoading = false - //Fragment에서 isLoading을 observe - private val _isLoading = MutableLiveData(false) - val isLoading:LiveData - get() = _isLoading + private val _homeState = MutableLiveData(UiState.Loading) + val homeState: LiveData + get() = _homeState fun loadUserList(page: Int) { viewModelScope.launch { - kotlin.runCatching { - _isLoading.value = true - delay(3000) + runCatching { + _homeState.value = UiState.Loading + delay(1000) homeRepository.loadUser(page) }.onSuccess { - _isLoading.value = false userList.value = it.data.toUserList() //서버에서 가져온 userList -> userList LiveData - _loadUserSuccess.value = true + _homeState.value = UiState.Success }.onFailure { - _isLoading.value = false - _loadUserSuccess.value = false + _homeState.value = UiState.Failure } } } - - } \ No newline at end of file diff --git a/app/src/main/java/org/sopt/sample/presentation/home/adapter/HomeUserListAdapter.kt b/app/src/main/java/org/sopt/sample/presentation/home/adapter/HomeUserListAdapter.kt index 2be88ee..65a9da9 100644 --- a/app/src/main/java/org/sopt/sample/presentation/home/adapter/HomeUserListAdapter.kt +++ b/app/src/main/java/org/sopt/sample/presentation/home/adapter/HomeUserListAdapter.kt @@ -5,7 +5,7 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide -import org.sopt.sample.data.home.model.User +import org.sopt.sample.data.model.User import org.sopt.sample.databinding.HomeUserProfileItemBinding class HomeUserListAdapter(private val context: Context) : diff --git a/app/src/main/java/org/sopt/sample/presentation/music/MusicFragment.kt b/app/src/main/java/org/sopt/sample/presentation/music/MusicFragment.kt new file mode 100644 index 0000000..daf3a35 --- /dev/null +++ b/app/src/main/java/org/sopt/sample/presentation/music/MusicFragment.kt @@ -0,0 +1,50 @@ +package org.sopt.sample.presentation.music + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.LinearLayoutManager +import dagger.hilt.android.AndroidEntryPoint +import org.sopt.sample.R +import org.sopt.sample.base.BindingFragment +import org.sopt.sample.databinding.FragmentMusicBinding +import org.sopt.sample.presentation.music.adapter.MusicListAdapter +import org.sopt.sample.presentation.music.data.MusicAdd +import org.sopt.sample.presentation.state.UiState + +@AndroidEntryPoint +class MusicFragment : BindingFragment(R.layout.fragment_music) { + private val viewModel: MusicViewModel by viewModels() + + private val adapter by lazy { + MusicListAdapter( + requireContext(), + childFragmentManager + ) //childFragmentManager 음악 추가 버튼 클릭 시, 다이얼로그 show() 호출을 위해 전달하는 인자 + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.viewModel = viewModel + viewModel.fetchMusicList() + addObserver() + initAdapter() + } + + private fun addObserver() { + viewModel.musicState.observe(viewLifecycleOwner) { + if (it is UiState.Success) { + viewModel.musicList.value?.let { data -> + adapter.setData(data) + adapter.addData(MusicAdd(R.drawable.ic_baseline_add_circle_outline_24)) + } + } + } + } + + private fun initAdapter() { + binding.musicRv.layoutManager = LinearLayoutManager(requireContext()) + binding.musicRv.adapter = adapter + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/sample/presentation/music/MusicViewModel.kt b/app/src/main/java/org/sopt/sample/presentation/music/MusicViewModel.kt new file mode 100644 index 0000000..895139d --- /dev/null +++ b/app/src/main/java/org/sopt/sample/presentation/music/MusicViewModel.kt @@ -0,0 +1,83 @@ +package org.sopt.sample.presentation.music + +import android.net.Uri +import androidx.lifecycle.* +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import org.json.JSONObject +import org.sopt.sample.application.ApplicationClass +import org.sopt.sample.domain.MusicRepository +import org.sopt.sample.presentation.music.data.MusicData +import org.sopt.sample.presentation.state.UiState +import org.sopt.sample.util.ContentUriRequestBody +import org.sopt.sample.util.addSourceList +import org.sopt.sample.util.extensions.toJsonRequestBody +import org.sopt.sample.util.extensions.toMusicInfo +import timber.log.Timber +import javax.inject.Inject + +@HiltViewModel +class MusicViewModel @Inject constructor( + private val musicRepository: MusicRepository, + private val application: ApplicationClass +) : + ViewModel() { + + private val _musicState = MutableLiveData(UiState.Loading) + val musicState: LiveData + get() = _musicState + + val musicList = MutableLiveData>() + + val title = MutableLiveData() + val singer = MutableLiveData() + val uri = MutableLiveData() + val isDialogInputValid = MediatorLiveData() + + init { + checkInputValid() + } + + fun fetchMusicList() { + viewModelScope.launch { + runCatching { + _musicState.value = UiState.Loading + musicRepository.fetchMusicList() + }.onSuccess { + musicList.value = it.data.toMusicInfo() + _musicState.value = UiState.Success + }.onFailure { + _musicState.value = UiState.Failure + Timber.e(it.message) + } + } + } + + fun uploadMusic() { + viewModelScope.launch { + val imageMultipartBody = ContentUriRequestBody( + application.baseContext, //ViewModel에 context가 들어가면 안된다..? + uri.value!! + ).toFormData() + val jsonRequestBody = + JSONObject("{\"singer\":\"${singer.value}\",\"title\":\"${title.value}\"}").toString() + .toJsonRequestBody() + runCatching { + _musicState.value = UiState.Loading + musicRepository.uploadMusic(jsonRequestBody, imageMultipartBody) + }.onSuccess { + + fetchMusicList()//음악 추가 후 리스트 불러오기 + }.onFailure { + _musicState.value = UiState.Failure + Timber.e(it.message) + } + } + } + + private fun checkInputValid() { + isDialogInputValid.addSourceList(title, singer, uri) { + uri.value == null || title.value.isNullOrEmpty() || singer.value.isNullOrEmpty() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/sample/presentation/music/adapter/MusicListAdapter.kt b/app/src/main/java/org/sopt/sample/presentation/music/adapter/MusicListAdapter.kt new file mode 100644 index 0000000..10c2d4a --- /dev/null +++ b/app/src/main/java/org/sopt/sample/presentation/music/adapter/MusicListAdapter.kt @@ -0,0 +1,112 @@ +package org.sopt.sample.presentation.music.adapter + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.fragment.app.FragmentManager +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding +import coil.load +import org.sopt.sample.databinding.MusicAddItemBinding +import org.sopt.sample.databinding.MusicInfoItemBinding +import org.sopt.sample.presentation.music.data.* +import org.sopt.sample.presentation.music.dialog.MusicAddDialog +import org.sopt.sample.util.DiffUtilItemCallback + +class MusicListAdapter( + private val context: Context, + private val childFragmentManager: FragmentManager +) : + ListAdapter(DiffUtilItemCallback()) { + private val inflater by lazy { LayoutInflater.from(context) } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MusicViewHolder { + return when (viewType) { + MUSIC_INFO_TYPE -> MusicInfoViewHolder( + MusicInfoItemBinding.inflate( + inflater, + parent, + false + ) + ) + MUSIC_ADD_TYPE -> MusicAddViewHolder( + childFragmentManager, + MusicAddItemBinding.inflate( + inflater, + parent, + false + ) + ) + else -> { + throw IllegalArgumentException("${this::class.java.simpleName} error") + } + } + + } + + override fun onBindViewHolder(holder: MusicViewHolder, position: Int) { + + holder.onBind(currentList[position]) + } + + override fun getItemCount(): Int = currentList.size + override fun getItemViewType(position: Int): Int { + val result = when (currentList[position]) { + is MusicInfo -> MUSIC_INFO_TYPE + is MusicAdd -> MUSIC_ADD_TYPE + else -> throw IllegalArgumentException("${this::class.java.simpleName} error") + } + return result + } + + fun setData(dataList: List) { + submitList(dataList) + } + + fun addData(data: MusicData) { + val newData = mutableListOf() + newData.addAll(currentList) + newData.add(data) + submitList(newData) + } + + fun removeData(position: Int) { + if (currentList.isNotEmpty()) { + val newData = mutableListOf() + newData.addAll(currentList) + newData.removeAt(position) + submitList(newData) + } + } +} + +abstract class MusicViewHolder(private val binding: ViewBinding) : + RecyclerView.ViewHolder(binding.root) { + abstract fun onBind(data: MusicData) +} + +class MusicInfoViewHolder(private val binding: MusicInfoItemBinding) : MusicViewHolder(binding) { + override fun onBind(data: MusicData) { + val infoData = data as MusicInfo + binding.musicItemIv.load(infoData.img) + binding.musicItemTitleTv.text = infoData.title + binding.musicItemSingerTv.text = infoData.singer + } +} + +class MusicAddViewHolder( + private val childFragmentManager: FragmentManager, //음악 추가 버튼 클릭 시, 다이얼로그 show() 호출을 위해 전달하는 인자 + private val binding: MusicAddItemBinding +) : MusicViewHolder(binding) { + override fun onBind(data: MusicData) { + val addData = data as MusicAdd + val dialog = MusicAddDialog() + binding.musicItemAddBtn.apply { + setImageResource(addData.addRes) + setOnClickListener { + dialog.isCancelable = false //배경클릭 금지 + dialog.show(childFragmentManager, "MusicAddDialog") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/sample/presentation/music/data/MusicData.kt b/app/src/main/java/org/sopt/sample/presentation/music/data/MusicData.kt new file mode 100644 index 0000000..66b057d --- /dev/null +++ b/app/src/main/java/org/sopt/sample/presentation/music/data/MusicData.kt @@ -0,0 +1,12 @@ +package org.sopt.sample.presentation.music.data + +sealed class MusicData + +const val MUSIC_INFO_TYPE = 0 +const val MUSIC_ADD_TYPE = 1 + +data class MusicInfo(val id: Int, val img: String, val title: String, val singer: String) : + MusicData() + +data class MusicAdd(val addRes:Int) : + MusicData() diff --git a/app/src/main/java/org/sopt/sample/presentation/music/dialog/MusicAddDialog.kt b/app/src/main/java/org/sopt/sample/presentation/music/dialog/MusicAddDialog.kt new file mode 100644 index 0000000..6fe97b0 --- /dev/null +++ b/app/src/main/java/org/sopt/sample/presentation/music/dialog/MusicAddDialog.kt @@ -0,0 +1,79 @@ +package org.sopt.sample.presentation.music.dialog + +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.view.View +import android.view.WindowManager +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.fragment.app.viewModels +import coil.load +import dagger.hilt.android.AndroidEntryPoint +import org.sopt.sample.R +import org.sopt.sample.base.BindingDialog +import org.sopt.sample.databinding.DialogMusicAddBinding +import org.sopt.sample.presentation.music.MusicViewModel +import org.sopt.sample.presentation.state.UiState + +@AndroidEntryPoint +class MusicAddDialog() : BindingDialog(R.layout.dialog_music_add) { + private val viewModel: MusicViewModel by viewModels() + private var pickVmLauncher: ActivityResultLauncher? = null + + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.viewModel = viewModel + binding.dialog = this + pickVmLauncher = registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { + binding.dialogImgIv.load(it) + viewModel.uri.value = it + } + initDialog() + addListener() + addObserver() + + } + + override fun onDestroyView() { + super.onDestroyView() + pickVmLauncher = null + } + + private fun initDialog() { + dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + dialog?.window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + } + + private fun addListener() { + binding.dialogImgIv.setOnClickListener { + pickVmLauncher?.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) + } + binding.dialogCancelBtn.setOnClickListener { + dismissDialog() + } + } + + private fun addObserver() { + viewModel.isDialogInputValid.observe(viewLifecycleOwner) { + binding.dialogConfirmBtn.isEnabled = !it + } + viewModel.musicState.observe(viewLifecycleOwner) { + if (it is UiState.Success) { + dismissDialog() + } + } + } + + fun dismissDialog() { + with(binding) { + dialogImgIv.load(null) + dialogInputEtTitle.text = null + dialogInputEtSinger.text = null + } + dialog?.dismiss() + } +} + diff --git a/app/src/main/java/org/sopt/sample/presentation/login/signin/SignInActivity.kt b/app/src/main/java/org/sopt/sample/presentation/sign/signin/SignInActivity.kt similarity index 96% rename from app/src/main/java/org/sopt/sample/presentation/login/signin/SignInActivity.kt rename to app/src/main/java/org/sopt/sample/presentation/sign/signin/SignInActivity.kt index 7605c2e..6d0773e 100644 --- a/app/src/main/java/org/sopt/sample/presentation/login/signin/SignInActivity.kt +++ b/app/src/main/java/org/sopt/sample/presentation/sign/signin/SignInActivity.kt @@ -1,4 +1,4 @@ -package org.sopt.sample.presentation.login.signin +package org.sopt.sample.presentation.sign.signin import android.annotation.SuppressLint import android.content.Intent @@ -11,7 +11,7 @@ import org.sopt.sample.R import org.sopt.sample.base.BindingActivity import org.sopt.sample.databinding.ActivitySigninBinding import org.sopt.sample.presentation.MainActivity -import org.sopt.sample.presentation.login.signup.SignupActivity +import org.sopt.sample.presentation.sign.signup.SignupActivity import org.sopt.sample.util.extensions.showToast @AndroidEntryPoint diff --git a/app/src/main/java/org/sopt/sample/presentation/login/signin/SignInViewModel.kt b/app/src/main/java/org/sopt/sample/presentation/sign/signin/SignInViewModel.kt similarity index 91% rename from app/src/main/java/org/sopt/sample/presentation/login/signin/SignInViewModel.kt rename to app/src/main/java/org/sopt/sample/presentation/sign/signin/SignInViewModel.kt index 695d180..5e25032 100644 --- a/app/src/main/java/org/sopt/sample/presentation/login/signin/SignInViewModel.kt +++ b/app/src/main/java/org/sopt/sample/presentation/sign/signin/SignInViewModel.kt @@ -1,10 +1,10 @@ -package org.sopt.sample.presentation.login.signin +package org.sopt.sample.presentation.sign.signin import androidx.lifecycle.* import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import org.sopt.sample.data.auth.model.SignInRequest -import org.sopt.sample.data.auth.repository.AuthRepository +import org.sopt.sample.data.model.SignInRequest +import org.sopt.sample.domain.AuthRepository import org.sopt.sample.util.addSourceList import javax.inject.Inject diff --git a/app/src/main/java/org/sopt/sample/presentation/login/signup/SignUpViewModel.kt b/app/src/main/java/org/sopt/sample/presentation/sign/signup/SignUpViewModel.kt similarity index 51% rename from app/src/main/java/org/sopt/sample/presentation/login/signup/SignUpViewModel.kt rename to app/src/main/java/org/sopt/sample/presentation/sign/signup/SignUpViewModel.kt index fbbde85..93d7c3f 100644 --- a/app/src/main/java/org/sopt/sample/presentation/login/signup/SignUpViewModel.kt +++ b/app/src/main/java/org/sopt/sample/presentation/sign/signup/SignUpViewModel.kt @@ -1,21 +1,26 @@ -package org.sopt.sample.presentation.login.signup +package org.sopt.sample.presentation.sign.signup import androidx.lifecycle.* import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import org.sopt.sample.data.auth.model.SignUpRequest -import org.sopt.sample.data.auth.repository.AuthRepository +import org.sopt.sample.data.model.SignUpRequest +import org.sopt.sample.domain.AuthRepository import org.sopt.sample.util.addSourceList import java.util.regex.Pattern import javax.inject.Inject + @HiltViewModel -class SignUpViewModel @Inject constructor(private val authRepository: AuthRepository) : ViewModel() { - //id - 6~10글자 영문 숫자 포함 - //pw - 6~12글자 영문,숫자,특수문자 포함 - private val idRegex = "^(?=.*[a-zA-Z]+)(?=.*[0-9]+).{6,10}$" - private val pwRegex = "^(?=.*[a-zA-Z]+)(?=.*[0-9]+)(?=.*[!@#$%^&*()~`<>?:']+).{6,12}$" - private val idMatcher: Pattern = Pattern.compile(idRegex) - private val pwMatcher: Pattern = Pattern.compile(pwRegex) +class SignUpViewModel @Inject constructor(private val authRepository: AuthRepository) : + ViewModel() { + + companion object { + //id - 6~10글자 영문 숫자 포함 + //pw - 6~12글자 영문,숫자,특수문자 포함 + private val REGEX_ID_PATTERN = Pattern.compile("^(?=.*[a-zA-Z]+)(?=.*[0-9]+).{6,10}$") + private val REGEX_PW_PATTERN = + Pattern.compile("^(?=.*[a-zA-Z]+)(?=.*[0-9]+)(?=.*[!@#$%^&*()~`<>?:']+).{6,12}$") + } + val name = MutableLiveData() val id = MutableLiveData() @@ -34,21 +39,22 @@ class SignUpViewModel @Inject constructor(private val authRepository: AuthRepos private fun isEnabledSignupButton() { isInputValid.apply { - addSourceList(name, id, pw) { inputValidCheck() } + addSourceList(name, id, pw) { checkInputValid() } } } - private fun inputValidCheck(): Boolean = isNameValid && isIdValid && isPwValid + private fun checkInputValid(): Boolean = isNameValid && isIdValid && isPwValid + val isNameValid: Boolean - get() = name.value?.let{ name.value!!.length >=2}==true + get() = name.value?.let { name.value!!.length >= 2 } == true val isIdValid: Boolean - get() = id.value?.let { idMatcher.matcher(it).matches() } == true + get() = id.value?.let { REGEX_ID_PATTERN.matcher(it).matches() } == true val isPwValid: Boolean - get() = pw.value?.let { pwMatcher.matcher(it).matches() } == true + get() = pw.value?.let { REGEX_PW_PATTERN.matcher(it).matches() } == true + + private fun signUpRequest(): SignUpRequest = + SignUpRequest(id.value.toString(), name.value.toString(), pw.value.toString()) - private fun request(): SignUpRequest { - return SignUpRequest(id.value.toString(), name.value.toString(), pw.value.toString()) - } private fun postSignUp(signUpRequest: SignUpRequest) { viewModelScope.launch { @@ -63,6 +69,6 @@ class SignUpViewModel @Inject constructor(private val authRepository: AuthRepos } fun signUp() { - postSignUp(request()) + postSignUp(signUpRequest()) } } \ No newline at end of file diff --git a/app/src/main/java/org/sopt/sample/presentation/login/signup/SignupActivity.kt b/app/src/main/java/org/sopt/sample/presentation/sign/signup/SignupActivity.kt similarity index 63% rename from app/src/main/java/org/sopt/sample/presentation/login/signup/SignupActivity.kt rename to app/src/main/java/org/sopt/sample/presentation/sign/signup/SignupActivity.kt index 8752e80..7c0c7b0 100644 --- a/app/src/main/java/org/sopt/sample/presentation/login/signup/SignupActivity.kt +++ b/app/src/main/java/org/sopt/sample/presentation/sign/signup/SignupActivity.kt @@ -1,18 +1,19 @@ -package org.sopt.sample.presentation.login.signup +package org.sopt.sample.presentation.sign.signup import android.content.Intent import android.os.Bundle import androidx.activity.viewModels +import com.google.android.material.textfield.TextInputLayout import dagger.hilt.android.AndroidEntryPoint import org.sopt.sample.R import org.sopt.sample.base.BindingActivity import org.sopt.sample.databinding.ActivitySignupBinding -import org.sopt.sample.presentation.login.signin.SignInActivity +import org.sopt.sample.presentation.sign.signin.SignInActivity import org.sopt.sample.util.extensions.showToast @AndroidEntryPoint class SignupActivity : BindingActivity(R.layout.activity_signup) { -// private val viewModel: SignUpViewModel by viewModels { ViewModelFactory() } + // private val viewModel: SignUpViewModel by viewModels { ViewModelFactory() } private val viewModel: SignUpViewModel by viewModels() companion object { @@ -25,9 +26,6 @@ class SignupActivity : BindingActivity(R.layout.activity_ binding.viewModel = viewModel binding.lifecycleOwner = this addObserver() - addListener() - - } private fun addObserver() { @@ -44,45 +42,34 @@ class SignupActivity : BindingActivity(R.layout.activity_ //name,id, pw 유효성 검사 viewModel.name.observe(this) { binding.signUpNameInput.apply { - if (!viewModel.isNameValid) { - isErrorEnabled = true - error = " " - } else { - isErrorEnabled = false - } + inputErrorCheck(this, viewModel.isNameValid) } } viewModel.id.observe(this) { binding.signUpIdInput.apply { - if (!viewModel.isIdValid) { - isErrorEnabled = true - error = " " - } else { - isErrorEnabled = false - } + inputErrorCheck(this, viewModel.isIdValid) } } viewModel.pw.observe(this) { binding.signUpPwInput.apply { - if (!viewModel.isPwValid) { - isErrorEnabled = true - error = " " - } else { - isErrorEnabled = false - } + inputErrorCheck(this, viewModel.isPwValid) } } } - //입력값 valid check -> signUp():서버통신 -> signUpSuccess값 변경 -> 회원가입 success or fail - private fun addListener() { - //뒤로가기 버튼 - binding.signUpBackBtn.setOnClickListener { - moveToSignIn() - } - } - private fun moveToSignIn() { + fun moveToSignIn() { startActivity(Intent(this, SignInActivity::class.java)) finish() } + + private fun inputErrorCheck(textInputLayout: TextInputLayout, isValid: Boolean) { + textInputLayout.apply { + if (!isValid) { + isErrorEnabled = true + error = " " + } else { + isErrorEnabled = false + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/sopt/sample/presentation/state/UiState.kt b/app/src/main/java/org/sopt/sample/presentation/state/UiState.kt new file mode 100644 index 0000000..3370456 --- /dev/null +++ b/app/src/main/java/org/sopt/sample/presentation/state/UiState.kt @@ -0,0 +1,8 @@ +package org.sopt.sample.presentation.state + +sealed class UiState { + object Success : UiState() + object Empty : UiState() + object Failure : UiState() + object Loading : UiState() +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/sample/splash/SplashActivity.kt b/app/src/main/java/org/sopt/sample/splash/SplashActivity.kt index a7c5f1a..adcaa1f 100644 --- a/app/src/main/java/org/sopt/sample/splash/SplashActivity.kt +++ b/app/src/main/java/org/sopt/sample/splash/SplashActivity.kt @@ -4,15 +4,13 @@ import android.content.Intent import android.os.Bundle import androidx.activity.viewModels import androidx.lifecycle.lifecycleScope -import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.sopt.sample.R import org.sopt.sample.base.BindingActivity import org.sopt.sample.databinding.ActivitySplashBinding import org.sopt.sample.presentation.MainActivity -import org.sopt.sample.presentation.login.signin.SignInActivity -import org.sopt.sample.util.EventObserver +import org.sopt.sample.presentation.sign.signin.SignInActivity import org.sopt.sample.util.extensions.showToast class SplashActivity : BindingActivity(R.layout.activity_splash) { diff --git a/app/src/main/java/org/sopt/sample/util/ContentUriRequestBody.kt b/app/src/main/java/org/sopt/sample/util/ContentUriRequestBody.kt new file mode 100644 index 0000000..9c874d0 --- /dev/null +++ b/app/src/main/java/org/sopt/sample/util/ContentUriRequestBody.kt @@ -0,0 +1,53 @@ +package org.sopt.sample.util + +import android.content.Context +import android.net.Uri +import android.provider.MediaStore +import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody +import okio.BufferedSink +import okio.source + +class ContentUriRequestBody( + context: Context, + private val uri: Uri +) : RequestBody() { + private val contentResolver = context.contentResolver + + private var fileName = "" + private var size = -1L + + init { + contentResolver.query( + uri, + arrayOf(MediaStore.Images.Media.SIZE, MediaStore.Images.Media.DISPLAY_NAME), + null, + null, + null + )?.use { cursor -> + if (cursor.moveToFirst()) { + size = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.SIZE)) + fileName = + cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME)) + } + } + } + + fun getFileName() = fileName + + override fun contentLength(): Long = size + + override fun contentType(): MediaType? = + contentResolver.getType(uri)?.toMediaTypeOrNull() + + override fun writeTo(sink: BufferedSink) { + contentResolver.openInputStream(uri)?.source()?.use { source -> + sink.writeAll(source) + } + } + /** uri -> file-> RequestBody **/ + + fun toFormData() = MultipartBody.Part.createFormData("image", getFileName(), this) +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/sample/util/DiffUtilItemCallback.kt b/app/src/main/java/org/sopt/sample/util/DiffUtilItemCallback.kt new file mode 100644 index 0000000..61ed8ea --- /dev/null +++ b/app/src/main/java/org/sopt/sample/util/DiffUtilItemCallback.kt @@ -0,0 +1,22 @@ +package org.sopt.sample.util + +import androidx.recyclerview.widget.DiffUtil + +interface DiffUtilEquals{ + fun equals(oldItem:T, newItem:T):Boolean? +} + +class DiffUtilItemCallback :DiffUtil.ItemCallback(), DiffUtilEquals { + + override fun equals(olditem: T, newItem: T): Boolean { + return olditem!! == newItem + } + + override fun areItemsTheSame(oldItem: T, newItem: T): Boolean { + return oldItem == newItem + } + + override fun areContentsTheSame(oldItem:T, newItem: T): Boolean { + return equals(oldItem,newItem) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/sample/util/extensions/ListExt.kt b/app/src/main/java/org/sopt/sample/util/extensions/ListExt.kt index 0db1e2f..4eb3fb7 100644 --- a/app/src/main/java/org/sopt/sample/util/extensions/ListExt.kt +++ b/app/src/main/java/org/sopt/sample/util/extensions/ListExt.kt @@ -1,7 +1,9 @@ package org.sopt.sample.util.extensions -import org.sopt.sample.data.home.model.User -import org.sopt.sample.data.home.model.UserInfo +import org.sopt.sample.data.model.Music +import org.sopt.sample.data.model.User +import org.sopt.sample.data.model.UserInfo +import org.sopt.sample.presentation.music.data.MusicInfo fun List.toUserList():List = this.map { it.toUser() } @@ -9,4 +11,13 @@ fun UserInfo.toUser() = User( email = this.email, avatar = this.avatar, name = this.firstname+this.lastname +) + +fun List.toMusicInfo():List = this.map { it.toMusicInfo() } + +fun Music.toMusicInfo() = MusicInfo( + id = this.id, + img = this.image, + title = this.title, + singer = this.singer ) \ No newline at end of file diff --git a/app/src/main/java/org/sopt/sample/util/extensions/StringExt.kt b/app/src/main/java/org/sopt/sample/util/extensions/StringExt.kt new file mode 100644 index 0000000..1a3be41 --- /dev/null +++ b/app/src/main/java/org/sopt/sample/util/extensions/StringExt.kt @@ -0,0 +1,8 @@ +package org.sopt.sample.util.extensions + +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.toRequestBody + +fun String?.toJsonRequestBody(): RequestBody = + requireNotNull(this).toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull()) \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/gallery_recycle_avengers_endgame.jpg b/app/src/main/res/drawable-v24/gallery_recycle_avengers_endgame.jpg deleted file mode 100644 index 66ee6e0..0000000 Binary files a/app/src/main/res/drawable-v24/gallery_recycle_avengers_endgame.jpg and /dev/null differ diff --git a/app/src/main/res/drawable-v24/gallery_recycle_ironman1.jpg b/app/src/main/res/drawable-v24/gallery_recycle_ironman1.jpg deleted file mode 100644 index 80f1fc3..0000000 Binary files a/app/src/main/res/drawable-v24/gallery_recycle_ironman1.jpg and /dev/null differ diff --git a/app/src/main/res/drawable-v24/gallery_recycle_ironman3.png b/app/src/main/res/drawable-v24/gallery_recycle_ironman3.png deleted file mode 100644 index efe8513..0000000 Binary files a/app/src/main/res/drawable-v24/gallery_recycle_ironman3.png and /dev/null differ diff --git a/app/src/main/res/drawable-v24/gallery_recycle_spiderman_amazing.jpg b/app/src/main/res/drawable-v24/gallery_recycle_spiderman_amazing.jpg deleted file mode 100644 index 9608e22..0000000 Binary files a/app/src/main/res/drawable-v24/gallery_recycle_spiderman_amazing.jpg and /dev/null differ diff --git a/app/src/main/res/drawable-v24/gallery_recycle_spiderman_farfromhome.jpg b/app/src/main/res/drawable-v24/gallery_recycle_spiderman_farfromhome.jpg deleted file mode 100644 index 230463b..0000000 Binary files a/app/src/main/res/drawable-v24/gallery_recycle_spiderman_farfromhome.jpg and /dev/null differ diff --git a/app/src/main/res/drawable-v24/gallery_recycle_spiderman_nowayhome.jpg b/app/src/main/res/drawable-v24/gallery_recycle_spiderman_nowayhome.jpg deleted file mode 100644 index c68774c..0000000 Binary files a/app/src/main/res/drawable-v24/gallery_recycle_spiderman_nowayhome.jpg and /dev/null differ diff --git a/app/src/main/res/drawable/dialog_border.xml b/app/src/main/res/drawable/dialog_border.xml new file mode 100644 index 0000000..c92f473 --- /dev/null +++ b/app/src/main/res/drawable/dialog_border.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/dialog_btn_selector.xml b/app/src/main/res/drawable/dialog_btn_selector.xml new file mode 100644 index 0000000..18d46a6 --- /dev/null +++ b/app/src/main/res/drawable/dialog_btn_selector.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/gallery_recycle_civilwar.jpg b/app/src/main/res/drawable/gallery_recycle_civilwar.jpg deleted file mode 100644 index 97e099c..0000000 Binary files a/app/src/main/res/drawable/gallery_recycle_civilwar.jpg and /dev/null differ diff --git a/app/src/main/res/drawable/gallery_recycle_docter_multiverse.jpg b/app/src/main/res/drawable/gallery_recycle_docter_multiverse.jpg deleted file mode 100644 index 3bc654a..0000000 Binary files a/app/src/main/res/drawable/gallery_recycle_docter_multiverse.jpg and /dev/null differ diff --git a/app/src/main/res/drawable/gallery_recycle_thor_darkworld.jpg b/app/src/main/res/drawable/gallery_recycle_thor_darkworld.jpg deleted file mode 100644 index 0cce9fc..0000000 Binary files a/app/src/main/res/drawable/gallery_recycle_thor_darkworld.jpg and /dev/null differ diff --git a/app/src/main/res/drawable/ic_baseline_add_circle_outline_24.xml b/app/src/main/res/drawable/ic_baseline_add_circle_outline_24.xml new file mode 100644 index 0000000..7d6ffb1 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_add_circle_outline_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_add_photo_alternate_24.xml b/app/src/main/res/drawable/ic_baseline_add_photo_alternate_24.xml new file mode 100644 index 0000000..147d60c --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_add_photo_alternate_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_library_music_24.xml b/app/src/main/res/drawable/ic_baseline_library_music_24.xml new file mode 100644 index 0000000..75203da --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_library_music_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/signup_btn_border.xml b/app/src/main/res/drawable/signup_btn_border.xml deleted file mode 100644 index 42e2393..0000000 --- a/app/src/main/res/drawable/signup_btn_border.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/signup_btn_border_disable.xml b/app/src/main/res/drawable/signup_btn_border_disable.xml deleted file mode 100644 index 524a9f1..0000000 --- a/app/src/main/res/drawable/signup_btn_border_disable.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/signup_btn_color.xml b/app/src/main/res/drawable/signup_btn_color.xml index d2049ff..5215527 100644 --- a/app/src/main/res/drawable/signup_btn_color.xml +++ b/app/src/main/res/drawable/signup_btn_color.xml @@ -1,5 +1,15 @@ - - + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_signin.xml b/app/src/main/res/layout/activity_signin.xml index 5263ac8..5fc4a62 100644 --- a/app/src/main/res/layout/activity_signin.xml +++ b/app/src/main/res/layout/activity_signin.xml @@ -5,7 +5,7 @@ + type="org.sopt.sample.presentation.sign.signin.SignInViewModel" /> + tools:context=".presentation.sign.signin.SignInActivity"> - + + + + type="org.sopt.sample.presentation.sign.signup.SignUpViewModel" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - + android:textSize="28sp" + android:textStyle="bold" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_music_add.xml b/app/src/main/res/layout/dialog_music_add.xml new file mode 100644 index 0000000..29a7824 --- /dev/null +++ b/app/src/main/res/layout/dialog_music_add.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + +