diff --git a/app/build.gradle b/app/build.gradle index 2244d7e..cf86fdf 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,8 +1,14 @@ plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' + id 'org.jetbrains.kotlin.plugin.serialization' version '1.8.20' + id 'kotlin-kapt' + id 'com.google.dagger.hilt.android' } +Properties properties = new Properties() +properties.load(project.rootProject.file('local.properties').newDataInputStream()) + android { namespace 'org.sopt.dosopttemplate' compileSdk 33 @@ -13,6 +19,8 @@ android { targetSdk 33 versionCode 1 versionName "1.0" + buildConfigField "String", "AUTH_BASE_URL", properties["auth.base.url"] + buildConfigField "String", "USER_BASE_URL", properties["user.base.url"] testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -24,27 +32,50 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_11 - targetCompatibility JavaVersion.VERSION_11 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '11' + jvmTarget = '17' } buildFeatures { viewBinding true + buildConfig true + dataBinding true } } dependencies { implementation 'com.github.bumptech.glide:glide:4.12.0' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2' annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0' implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2" implementation "androidx.fragment:fragment-ktx:1.6.1" - implementation 'androidx.core:core-ktx:1.8.0' + implementation 'androidx.core:core-ktx:1.9.0' implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.5.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1' + implementation 'com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9' + + // hilt + implementation 'com.google.dagger:hilt-android:2.44' + kapt 'com.google.dagger:hilt-android-compiler:2.44' + kapt 'com.google.dagger:dagger-android-processor:2.44' + + // timber + implementation "com.jakewharton.timber:timber:4.7.1" + +// define a BOM and its version + implementation(platform("com.squareup.okhttp3:okhttp-bom:4.10.0")) + +// define any required OkHttp artifacts without version + implementation("com.squareup.okhttp3:okhttp") + implementation("com.squareup.okhttp3:logging-interceptor") } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e568860..ef606fc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,7 +2,13 @@ + + + + + - + diff --git a/app/src/main/java/org/sopt/dosopttemplate/HomeActivity.kt b/app/src/main/java/org/sopt/dosopttemplate/HomeActivity.kt deleted file mode 100644 index 046f19e..0000000 --- a/app/src/main/java/org/sopt/dosopttemplate/HomeActivity.kt +++ /dev/null @@ -1,72 +0,0 @@ -package org.sopt.dosopttemplate - -import android.os.Bundle -import android.widget.Toast -import androidx.appcompat.app.AppCompatActivity -import androidx.fragment.app.Fragment -import org.sopt.dosopttemplate.databinding.ActivityHomeBinding - -class HomeActivity : AppCompatActivity() { - private lateinit var binding: ActivityHomeBinding - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - binding = ActivityHomeBinding.inflate(layoutInflater) - setContentView(binding.root) - - val currentFragment = supportFragmentManager.findFragmentById(R.id.fcv_home) - if (currentFragment == null){ - supportFragmentManager.beginTransaction() - .add(R.id.fcv_home, HomeFragment()) - .commit() - } - clickBottomNavigation() - } - - companion object { - fun createMyPageFragment(user_id: String?, user_major: String?): MyPageFragment { - return if (user_id != null && user_major != null) { - MyPageFragment.newInstance(user_id, user_major) - } else { - throw IllegalArgumentException("데이터가 없어요!") - } - } - } - - private fun clickBottomNavigation(){ - binding.bnvHome.setOnItemSelectedListener { - when (it.itemId) { - R.id.menu_home-> { - replaceFragment(HomeFragment()) - true - } - - R.id.menu_do_android-> { - replaceFragment(DoAndroidFragment()) - true - } - - R.id.menu_mypage -> { - try { - val user_id = intent?.getStringExtra("user_id") - val user_major = intent?.getStringExtra("user_major") - val myPageFragment = createMyPageFragment(user_id, user_major) - replaceFragment(myPageFragment) - } catch (e: IllegalArgumentException) { - Toast.makeText(this, e.message, Toast.LENGTH_SHORT).show() - } - true - } - - else -> false - } - } - } - - private fun replaceFragment(fragment: Fragment) { - supportFragmentManager.beginTransaction() - .replace(R.id.fcv_home, fragment) - .commit() - - } -} - diff --git a/app/src/main/java/org/sopt/dosopttemplate/LoginActivity.kt b/app/src/main/java/org/sopt/dosopttemplate/LoginActivity.kt deleted file mode 100644 index 23b3608..0000000 --- a/app/src/main/java/org/sopt/dosopttemplate/LoginActivity.kt +++ /dev/null @@ -1,64 +0,0 @@ -package org.sopt.dosopttemplate -import android.content.Intent -import android.os.Bundle -import android.util.Log -import android.widget.Toast -import androidx.activity.result.ActivityResultLauncher -import androidx.activity.result.contract.ActivityResultContracts -import androidx.appcompat.app.AppCompatActivity -import org.sopt.dosopttemplate.databinding.ActivityLoginBinding - -class LoginActivity : AppCompatActivity() { - private lateinit var binding: ActivityLoginBinding - lateinit var enteredId: String - lateinit var enteredPassword: String - lateinit var enteredMajor: String - lateinit var enteredName: String - lateinit var resultLauncher: ActivityResultLauncher - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - binding = ActivityLoginBinding.inflate(layoutInflater) - setContentView(binding.root) - - val signButton = binding.signupButton - val loginButton = binding.loginButton - - resultLauncher = registerForActivityResult( - ActivityResultContracts.StartActivityForResult() - ) { result -> - if (result.resultCode == RESULT_OK) { - val data: Intent? = result.data - enteredId = data?.getStringExtra("entered_id") ?: "" - enteredPassword = data?.getStringExtra("entered_password") ?: "" - enteredMajor = data?.getStringExtra("entered_Major") ?: "" - enteredName = data?.getStringExtra("entered_Name") ?: "" - } - } - - signButton.setOnClickListener { - val intent = Intent(this, SignUpActivity::class.java) - resultLauncher.launch(intent) - } - - loginButton.setOnClickListener { - - val inputId = binding.inputTextId - val inputPw = binding.inputTextPw - - val isLoginSuccessful = - enteredId == inputId.text.toString() && enteredPassword == inputPw.text.toString() - if (isLoginSuccessful) { - Toast.makeText(this, "로그인에 성공했습니다 :)", Toast.LENGTH_LONG).show() - - val mainIntent = Intent(this, HomeActivity::class.java) - mainIntent.putExtra("user_id", enteredId) - mainIntent.putExtra("user_major", enteredMajor) - startActivity(mainIntent) - } else { - Toast.makeText(this, "로그인에 실패했습니다 :(", Toast.LENGTH_LONG).show() - } - } - } - } - diff --git a/app/src/main/java/org/sopt/dosopttemplate/MyApplication.kt b/app/src/main/java/org/sopt/dosopttemplate/MyApplication.kt new file mode 100644 index 0000000..a1b4fa5 --- /dev/null +++ b/app/src/main/java/org/sopt/dosopttemplate/MyApplication.kt @@ -0,0 +1,15 @@ +package org.sopt.dosopttemplate + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp +import timber.log.Timber + +@HiltAndroidApp +class MyApplication : Application() { + override fun onCreate() { + super.onCreate() + if (BuildConfig.DEBUG) { + Timber.plant(Timber.DebugTree()) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/dosopttemplate/SignUpActivity.kt b/app/src/main/java/org/sopt/dosopttemplate/SignUpActivity.kt deleted file mode 100644 index 53272c7..0000000 --- a/app/src/main/java/org/sopt/dosopttemplate/SignUpActivity.kt +++ /dev/null @@ -1,59 +0,0 @@ -package org.sopt.dosopttemplate - -import android.app.Activity -import android.content.Intent -import android.os.Bundle -import android.widget.Toast -import androidx.appcompat.app.AppCompatActivity -import org.sopt.dosopttemplate.databinding.ActivitySignupBinding - -class SignUpActivity : AppCompatActivity() { - private lateinit var binding: ActivitySignupBinding - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - binding = ActivitySignupBinding.inflate(layoutInflater) - setContentView(binding.root) - - val signupButton = binding.signupButton - val editTextId = binding.editTextId - val editTextPassword = binding.editTextPassword - val editTextMajor = binding.editTextMajor - val editTextName = binding.editTextName - - signupButton.setOnClickListener { - val enteredId = editTextId.text.toString() - val enteredPassword = editTextPassword.text.toString() - val enteredMajor = editTextMajor.text.toString() - val enteredName = editTextName.text.toString() - - if (isValidSignUp(enteredId, enteredPassword, enteredMajor, enteredName)) { - val loginIntent = Intent() - loginIntent.putExtra("entered_id", enteredId) - loginIntent.putExtra("entered_password", enteredPassword) - loginIntent.putExtra("entered_Major", enteredMajor) - loginIntent.putExtra("entered_Name", enteredName) - setResult(RESULT_OK, loginIntent) - finish() - } else { - Toast.makeText(this, "회원가입 조건을 다시 확인하세요 !", Toast.LENGTH_SHORT).show() - } - } - } - - private fun isValidSignUp( - enteredId: String, - enteredPassword: String, - enteredMajor: String, - enteredName: String - ): Boolean { - - val isIdValid = enteredId.length >= 6 && enteredId.length <= 10 - val isPasswordValid = enteredPassword.length >= 8 && enteredPassword.length <= 12 - val isNicknameValid = enteredName.length >= 1 && !enteredName.isBlank() - val isMajorValid = enteredMajor.length >= 1 && !enteredMajor.isBlank() - - return isIdValid && isPasswordValid && isNicknameValid && isMajorValid - } -} diff --git a/app/src/main/java/org/sopt/dosopttemplate/data/datasource/UserDataSource.kt b/app/src/main/java/org/sopt/dosopttemplate/data/datasource/UserDataSource.kt new file mode 100644 index 0000000..02e809a --- /dev/null +++ b/app/src/main/java/org/sopt/dosopttemplate/data/datasource/UserDataSource.kt @@ -0,0 +1,7 @@ +package org.sopt.dosopttemplate.data.datasource + +import org.sopt.dosopttemplate.data.model.response.ResponseUserDto + +interface UserDataSource { + suspend fun getUser(page : Int): ResponseUserDto +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/dosopttemplate/data/datasourceimpl/UserDataSourceImpl.kt b/app/src/main/java/org/sopt/dosopttemplate/data/datasourceimpl/UserDataSourceImpl.kt new file mode 100644 index 0000000..cc40b6e --- /dev/null +++ b/app/src/main/java/org/sopt/dosopttemplate/data/datasourceimpl/UserDataSourceImpl.kt @@ -0,0 +1,14 @@ +package org.sopt.dosopttemplate.data.datasourceimpl + +import org.sopt.dosopttemplate.data.service.UserService +import org.sopt.dosopttemplate.data.model.response.ResponseUserDto +import org.sopt.dosopttemplate.data.datasource.UserDataSource +import javax.inject.Inject + +class UserDataSourceImpl @Inject constructor( + private val UserService: UserService + ): UserDataSource { + + override suspend fun getUser(page: Int): ResponseUserDto = + UserService.getUserList(page) +} diff --git a/app/src/main/java/org/sopt/dosopttemplate/data/model/request/RequestLoginDto.kt b/app/src/main/java/org/sopt/dosopttemplate/data/model/request/RequestLoginDto.kt new file mode 100644 index 0000000..b0f0ab1 --- /dev/null +++ b/app/src/main/java/org/sopt/dosopttemplate/data/model/request/RequestLoginDto.kt @@ -0,0 +1,12 @@ +package org.sopt.dosopttemplate.data.model.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RequestLoginDto( + @SerialName("username") + val username: String, + @SerialName("password") + val password: String, +) diff --git a/app/src/main/java/org/sopt/dosopttemplate/data/model/request/RequestSignupDto.kt b/app/src/main/java/org/sopt/dosopttemplate/data/model/request/RequestSignupDto.kt new file mode 100644 index 0000000..173d016 --- /dev/null +++ b/app/src/main/java/org/sopt/dosopttemplate/data/model/request/RequestSignupDto.kt @@ -0,0 +1,16 @@ +package org.sopt.dosopttemplate.data.model.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RequestSignupDto( + @SerialName("username") + val username: String, + @SerialName("password") + val password: String, + @SerialName("major") + val major: String, + @SerialName("nickname") + val nickname: String, +) diff --git a/app/src/main/java/org/sopt/dosopttemplate/data/model/response/ResponseLoginDto.kt b/app/src/main/java/org/sopt/dosopttemplate/data/model/response/ResponseLoginDto.kt new file mode 100644 index 0000000..ecbe174 --- /dev/null +++ b/app/src/main/java/org/sopt/dosopttemplate/data/model/response/ResponseLoginDto.kt @@ -0,0 +1,14 @@ +package org.sopt.dosopttemplate.data.model.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseLoginDto( + @SerialName("id") + val id: Int, + @SerialName("username") + val username: String, + @SerialName("nickname") + val nickname: String, +) diff --git a/app/src/main/java/org/sopt/dosopttemplate/data/model/response/ResponseUserDto.kt b/app/src/main/java/org/sopt/dosopttemplate/data/model/response/ResponseUserDto.kt new file mode 100644 index 0000000..319e1f7 --- /dev/null +++ b/app/src/main/java/org/sopt/dosopttemplate/data/model/response/ResponseUserDto.kt @@ -0,0 +1,50 @@ +package org.sopt.dosopttemplate.data.model.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import org.sopt.dosopttemplate.domain.entity.UserEntity + +@Serializable +data class ResponseUserDto( + @SerialName("page") + val page: Int, + @SerialName("per_page") + val perPage: Int, + @SerialName("total") + val total: Int, + @SerialName("total_pages") + val totalPages: Int, + @SerialName("data") + val data: List, + @SerialName("support") + val support: Support, +) { + @Serializable + data class ResponseUserData( + @SerialName("id") + val id: Int, + @SerialName("email") + val email: String = "", + @SerialName("first_name") + val firstName: String = "", + @SerialName("last_name") + val lastName: String = "", + @SerialName("avatar") + val avatar: String = "" + ) + @Serializable + data class Support( + @SerialName("url") + val url: String = "", + @SerialName("text") + val text: String = "" + ) + fun toUserEntity(): List = data.map { + UserEntity( + firstName = it.firstName, + email = it.email, + avatar = it.avatar + ) + } +} + diff --git a/app/src/main/java/org/sopt/dosopttemplate/data/repository/UserRepositoryImpl.kt b/app/src/main/java/org/sopt/dosopttemplate/data/repository/UserRepositoryImpl.kt new file mode 100644 index 0000000..e828afb --- /dev/null +++ b/app/src/main/java/org/sopt/dosopttemplate/data/repository/UserRepositoryImpl.kt @@ -0,0 +1,16 @@ +package org.sopt.dosopttemplate.data.repository + +import org.sopt.dosopttemplate.domain.repository.UserRepository +import org.sopt.dosopttemplate.data.datasource.UserDataSource +import org.sopt.dosopttemplate.domain.entity.UserEntity +import javax.inject.Inject + +class UserRepositoryImpl @Inject constructor( + private val userDataSource: UserDataSource + ): UserRepository { + + override suspend fun getUserList(page: Int): Result> = + runCatching { + userDataSource.getUser(page).toUserEntity() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/dosopttemplate/data/service/AuthService.kt b/app/src/main/java/org/sopt/dosopttemplate/data/service/AuthService.kt new file mode 100644 index 0000000..325895a --- /dev/null +++ b/app/src/main/java/org/sopt/dosopttemplate/data/service/AuthService.kt @@ -0,0 +1,19 @@ +package org.sopt.dosopttemplate.data.service + +import org.sopt.dosopttemplate.data.model.request.RequestLoginDto +import org.sopt.dosopttemplate.data.model.request.RequestSignupDto +import org.sopt.dosopttemplate.data.model.response.ResponseLoginDto +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.POST + +interface AuthService { + @POST("api/v1/members") + fun postSignUp( + @Body request: RequestSignupDto, + ): Call + @POST("api/v1/members/sign-in") + fun postLogin( + @Body request: RequestLoginDto, + ): Call +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/dosopttemplate/data/service/UserService.kt b/app/src/main/java/org/sopt/dosopttemplate/data/service/UserService.kt new file mode 100644 index 0000000..0899cf0 --- /dev/null +++ b/app/src/main/java/org/sopt/dosopttemplate/data/service/UserService.kt @@ -0,0 +1,14 @@ +package org.sopt.dosopttemplate.data.service + +import org.sopt.dosopttemplate.data.model.response.ResponseUserDto +import retrofit2.http.GET +import retrofit2.http.Query + +interface UserService { + @GET("api/users") + suspend fun getUserList( + @Query("page") page: Int + ): ResponseUserDto +} + + diff --git a/app/src/main/java/org/sopt/dosopttemplate/di/ApiFactory.kt b/app/src/main/java/org/sopt/dosopttemplate/di/ApiFactory.kt new file mode 100644 index 0000000..a8ba384 --- /dev/null +++ b/app/src/main/java/org/sopt/dosopttemplate/di/ApiFactory.kt @@ -0,0 +1,46 @@ +package org.sopt.dosopttemplate.di + +import android.util.Log +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import kotlinx.serialization.json.Json +import okhttp3.Interceptor +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import org.sopt.dosopttemplate.data.service.AuthService +import org.sopt.dosopttemplate.BuildConfig +import org.sopt.dosopttemplate.data.service.UserService +import retrofit2.Retrofit + +object ApiFactory { + const val AUTH_BASE_URL = BuildConfig.AUTH_BASE_URL + const val USER_BASE_URL = BuildConfig.USER_BASE_URL + + private fun getLogOkHttpClient(): Interceptor { + val loggingInterceptor = HttpLoggingInterceptor { message -> + Log.d("Retrofit2", "CONNECTION INFO -> $message") + } + loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY + return loggingInterceptor + } + + val okHttpClient = OkHttpClient.Builder() + .addInterceptor(getLogOkHttpClient()) + .build() + + inline fun create(baseUrl: String): T { + val retrofit = Retrofit.Builder() + .baseUrl(baseUrl) + .client(okHttpClient) + .addConverterFactory(Json.asConverterFactory("application/json".toMediaType())) + .build() + + return retrofit.create(T::class.java) + } +} + +object ServicePool { + val authService = ApiFactory.create(ApiFactory.AUTH_BASE_URL) + //val userService = ApiFactory.create(ApiFactory.USER_BASE_URL) +} + diff --git a/app/src/main/java/org/sopt/dosopttemplate/di/BindsModule.kt b/app/src/main/java/org/sopt/dosopttemplate/di/BindsModule.kt new file mode 100644 index 0000000..de3057a --- /dev/null +++ b/app/src/main/java/org/sopt/dosopttemplate/di/BindsModule.kt @@ -0,0 +1,25 @@ +package org.sopt.dosopttemplate.di + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import org.sopt.dosopttemplate.data.datasource.UserDataSource +import org.sopt.dosopttemplate.data.datasourceimpl.UserDataSourceImpl +import org.sopt.dosopttemplate.data.repository.UserRepositoryImpl +import org.sopt.dosopttemplate.domain.repository.UserRepository +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class BindModule { + + @Binds + @Singleton + abstract fun bindUserDataSource(userDataSourceImpl: UserDataSourceImpl): UserDataSource + + @Binds + @Singleton + abstract fun bindUserRepository(userRepositoryImpl: UserRepositoryImpl): UserRepository + +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/dosopttemplate/di/Qualifier.kt b/app/src/main/java/org/sopt/dosopttemplate/di/Qualifier.kt new file mode 100644 index 0000000..dd967be --- /dev/null +++ b/app/src/main/java/org/sopt/dosopttemplate/di/Qualifier.kt @@ -0,0 +1,7 @@ +package org.sopt.dosopttemplate.di + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class USER \ No newline at end of file diff --git a/app/src/main/java/org/sopt/dosopttemplate/di/RetrofitModule.kt b/app/src/main/java/org/sopt/dosopttemplate/di/RetrofitModule.kt new file mode 100644 index 0000000..7b378d4 --- /dev/null +++ b/app/src/main/java/org/sopt/dosopttemplate/di/RetrofitModule.kt @@ -0,0 +1,59 @@ +package org.sopt.dosopttemplate.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.dosopttemplate.BuildConfig.USER_BASE_URL +import retrofit2.Converter +import retrofit2.Retrofit +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object RetrofitModule { + private const val APPLICATION_JSON = "application/json" + + @Provides + @Singleton + fun provideJson(): Json = Json { + ignoreUnknownKeys = true + prettyPrint = true + } + + @Provides + @Singleton + fun provideJsonConverter(json: Json): Converter.Factory= + json.asConverterFactory(APPLICATION_JSON.toMediaType()) + + @Provides + @Singleton + fun provideHttpLoggingInterceptor(): Interceptor = HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + } + + @Provides + @Singleton + fun provideOkHttpClient( + loggingInterceptor: Interceptor, + ): OkHttpClient = OkHttpClient.Builder() + .addInterceptor(loggingInterceptor) + .build() + + @Provides + @Singleton + fun provideRetrofit( + client: OkHttpClient, + factory: Converter.Factory, + ): Retrofit = Retrofit.Builder() + .baseUrl(USER_BASE_URL) + .client(client) + .addConverterFactory(factory) + .build() +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/dosopttemplate/di/ServiceModule.kt b/app/src/main/java/org/sopt/dosopttemplate/di/ServiceModule.kt new file mode 100644 index 0000000..becdfbd --- /dev/null +++ b/app/src/main/java/org/sopt/dosopttemplate/di/ServiceModule.kt @@ -0,0 +1,18 @@ +package org.sopt.dosopttemplate.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import org.sopt.dosopttemplate.data.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/dosopttemplate/domain/entity/UserEntity.kt b/app/src/main/java/org/sopt/dosopttemplate/domain/entity/UserEntity.kt new file mode 100644 index 0000000..4b2bdf9 --- /dev/null +++ b/app/src/main/java/org/sopt/dosopttemplate/domain/entity/UserEntity.kt @@ -0,0 +1,7 @@ +package org.sopt.dosopttemplate.domain.entity + +data class UserEntity( + val firstName: String, + val email: String, + val avatar: String +) diff --git a/app/src/main/java/org/sopt/dosopttemplate/domain/repository/UserRepository.kt b/app/src/main/java/org/sopt/dosopttemplate/domain/repository/UserRepository.kt new file mode 100644 index 0000000..b00cea7 --- /dev/null +++ b/app/src/main/java/org/sopt/dosopttemplate/domain/repository/UserRepository.kt @@ -0,0 +1,7 @@ +package org.sopt.dosopttemplate.domain.repository + +import org.sopt.dosopttemplate.domain.entity.UserEntity + +interface UserRepository { + suspend fun getUserList(page: Int): Result> +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/dosopttemplate/DoAndroidFragment.kt b/app/src/main/java/org/sopt/dosopttemplate/presentation/doandroid/DoAndroidFragment.kt similarity index 94% rename from app/src/main/java/org/sopt/dosopttemplate/DoAndroidFragment.kt rename to app/src/main/java/org/sopt/dosopttemplate/presentation/doandroid/DoAndroidFragment.kt index 34cc61f..f0fbb91 100644 --- a/app/src/main/java/org/sopt/dosopttemplate/DoAndroidFragment.kt +++ b/app/src/main/java/org/sopt/dosopttemplate/presentation/doandroid/DoAndroidFragment.kt @@ -1,4 +1,4 @@ -package org.sopt.dosopttemplate +package org.sopt.dosopttemplate.presentation.doandroid import android.os.Bundle import androidx.fragment.app.Fragment diff --git a/app/src/main/java/org/sopt/dosopttemplate/FriendViewHolder.kt b/app/src/main/java/org/sopt/dosopttemplate/presentation/home/FriendViewHolder.kt similarity index 58% rename from app/src/main/java/org/sopt/dosopttemplate/FriendViewHolder.kt rename to app/src/main/java/org/sopt/dosopttemplate/presentation/home/FriendViewHolder.kt index 8253012..0e4b14d 100644 --- a/app/src/main/java/org/sopt/dosopttemplate/FriendViewHolder.kt +++ b/app/src/main/java/org/sopt/dosopttemplate/presentation/home/FriendViewHolder.kt @@ -1,4 +1,4 @@ -package org.sopt.dosopttemplate +package org.sopt.dosopttemplate.presentation.home import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide @@ -7,13 +7,14 @@ import org.sopt.dosopttemplate.databinding.ItemFriendBinding class FriendViewHolder(private val binding: ItemFriendBinding) : RecyclerView.ViewHolder(binding.root) { - fun onBind(userUserProfileData: UserProfile.User) { + fun onBind(RealUserData: RealUserProfile.RealUser) { with(binding) { Glide.with(ivProfile) - .load(userUserProfileData.profileImage) + .load(RealUserData.profileImage) .into(ivProfile) - tvName.text = userUserProfileData.name - tvSelfMessage.text = userUserProfileData.message + tvName.text = RealUserData.name + tvSelfMessage.text = RealUserData.message } } } + diff --git a/app/src/main/java/org/sopt/dosopttemplate/presentation/home/HomeActivity.kt b/app/src/main/java/org/sopt/dosopttemplate/presentation/home/HomeActivity.kt new file mode 100644 index 0000000..b7aa3ad --- /dev/null +++ b/app/src/main/java/org/sopt/dosopttemplate/presentation/home/HomeActivity.kt @@ -0,0 +1,165 @@ +package org.sopt.dosopttemplate.presentation.home + +import android.animation.ObjectAnimator +import android.os.Bundle +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import org.sopt.dosopttemplate.presentation.doandroid.DoAndroidFragment +import org.sopt.dosopttemplate.presentation.mypage.MyPageFragment +import org.sopt.dosopttemplate.R +import org.sopt.dosopttemplate.databinding.ActivityHomeBinding +import org.sopt.dosopttemplate.presentation.user.UserFragment + + +class HomeActivity : AppCompatActivity() { + private lateinit var binding: ActivityHomeBinding + private var openFAB = false + private val WRITE_EXTERNAL_STORAGE_REQUEST_CODE = 1 + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityHomeBinding.inflate(layoutInflater) + setContentView(binding.root) + + val currentFragment = supportFragmentManager.findFragmentById(R.id.fcv_home) + if (currentFragment == null) { + supportFragmentManager.beginTransaction() + .add(R.id.fcv_home, HomeFragment()) + .commit() + } + clickBottomNavigation() + setCLICKFAB() + //checkManageExternalStoragePermission() + } + + companion object { + fun createMyPageFragment(user_id: String?, user_major: String?): MyPageFragment { + return if (user_id != null && user_major != null) { + MyPageFragment.newInstance(user_id, user_major) + } else { + throw IllegalArgumentException("데이터가 없어요!") + } + } + } + + private fun clickBottomNavigation() { + binding.bnvHome.setOnItemSelectedListener { + when (it.itemId) { + R.id.menu_home -> { + replaceFragment(UserFragment()) + true + } + + R.id.menu_do_android -> { + replaceFragment(DoAndroidFragment()) + true + } + + R.id.menu_mypage -> { + try { + val user_id = intent?.getStringExtra("user_id") + val user_major = intent?.getStringExtra("user_major") + val myPageFragment = createMyPageFragment(user_id, user_major) + replaceFragment(myPageFragment) + } catch (e: IllegalArgumentException) { + Toast.makeText(this, e.message, Toast.LENGTH_SHORT).show() + } + true + } + + else -> false + } + } + } + + private fun replaceFragment(fragment: Fragment) { + supportFragmentManager.beginTransaction() + .replace(R.id.fcv_home, fragment) + .commit() + + } + + private fun setCLICKFAB() { + binding.fabMain.setOnClickListener { + eventFAB() + } + binding.fabCapture.setOnClickListener { + //takeScreenshot() + } + + binding.fabShare.setOnClickListener { + Toast.makeText(this, "공유하는 중", Toast.LENGTH_SHORT).show() + } + } + + private fun eventFAB() { + if (openFAB) { + ObjectAnimator.ofFloat(binding.fabShare, "translationY", 0f).apply { start() } + ObjectAnimator.ofFloat(binding.fabCapture, "translationY", 0f).apply { start() } + } else { + ObjectAnimator.ofFloat(binding.fabShare, "translationY", -400f).apply { start() } + ObjectAnimator.ofFloat(binding.fabCapture, "translationY", -200f).apply { start() } + } + openFAB = !openFAB + } + + /*private fun checkManageExternalStoragePermission() { + val managePermission = ContextCompat.checkSelfPermission( + this, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) + + if (managePermission != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions( + this, + arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), + WRITE_EXTERNAL_STORAGE_REQUEST_CODE + ) + } + } + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + + if (requestCode == WRITE_EXTERNAL_STORAGE_REQUEST_CODE) { + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + Toast.makeText(this, "권한이 허용되었습니다.", Toast.LENGTH_SHORT).show() + } else { + Toast.makeText(this, "권한이 거부되었습니다.", Toast.LENGTH_SHORT).show() + } + } + } + + private fun takeScreenshot() { + val rootView = window.decorView.rootView + val screenShot: File? = captureScreen(rootView) + } + private fun captureScreen(view: View): File? { + val screenBitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888) + val canvas = Canvas(screenBitmap) + view.draw(canvas) + + if (screenBitmap.byteCount == 0) { + return null + } + + val filename = "Profilescreenshot.png" + val file = File(getExternalFilesDir(Environment.DIRECTORY_PICTURES), filename) + + try { + val output = FileOutputStream(file) + screenBitmap.compress(Bitmap.CompressFormat.PNG, 100, output) + output.close() + + } catch (e: IOException) { + e.printStackTrace() + return null + } + + return file + }*/ +} + diff --git a/app/src/main/java/org/sopt/dosopttemplate/HomeFragment.kt b/app/src/main/java/org/sopt/dosopttemplate/presentation/home/HomeFragment.kt similarity index 90% rename from app/src/main/java/org/sopt/dosopttemplate/HomeFragment.kt rename to app/src/main/java/org/sopt/dosopttemplate/presentation/home/HomeFragment.kt index 12b292a..159a300 100644 --- a/app/src/main/java/org/sopt/dosopttemplate/HomeFragment.kt +++ b/app/src/main/java/org/sopt/dosopttemplate/presentation/home/HomeFragment.kt @@ -1,6 +1,5 @@ -package org.sopt.dosopttemplate +package org.sopt.dosopttemplate.presentation.home -import MainAdapter import android.os.Bundle import androidx.fragment.app.Fragment import android.view.LayoutInflater @@ -29,7 +28,7 @@ class HomeFragment : Fragment() { super.onViewCreated(view, savedInstanceState) val MainAdapter = MainAdapter(requireContext()) binding.rvFriends.adapter = MainAdapter - MainAdapter.profileList = viewModel.mockUserProfileLists + MainAdapter.profileList = viewModel.mockRealUserProfileLists } override fun onDestroyView() { diff --git a/app/src/main/java/org/sopt/dosopttemplate/HomeViewModel.kt b/app/src/main/java/org/sopt/dosopttemplate/presentation/home/HomeViewModel.kt similarity index 68% rename from app/src/main/java/org/sopt/dosopttemplate/HomeViewModel.kt rename to app/src/main/java/org/sopt/dosopttemplate/presentation/home/HomeViewModel.kt index 61402a6..ff3618c 100644 --- a/app/src/main/java/org/sopt/dosopttemplate/HomeViewModel.kt +++ b/app/src/main/java/org/sopt/dosopttemplate/presentation/home/HomeViewModel.kt @@ -1,65 +1,66 @@ -package org.sopt.dosopttemplate +package org.sopt.dosopttemplate.presentation.home import androidx.lifecycle.ViewModel +import org.sopt.dosopttemplate.R class HomeViewModel : ViewModel() { - val mockUserProfileLists = mutableListOf( - UserProfile.My ( - profileImage = R.drawable.boong1, - name = "조세연", - message = "붕어의 계절티비", - ), - UserProfile.User( + val mockRealUserProfileLists = mutableListOf( + RealUserProfile.My( + profileImage = R.drawable.boong1, + name = "조세연", + message = "붕어의 계절티비", + ), + RealUserProfile.RealUser( profileImage = R.drawable.myimage, name = "경지현", message = "비티비타오백" ), - UserProfile.User( + RealUserProfile.RealUser( profileImage = R.drawable.myimage, name = "박강희", message = "오늘 생일티비!" ), - UserProfile.User( + RealUserProfile.RealUser( profileImage = R.drawable.myimage, name = "이삭", message = "안드짱티비" ), - UserProfile.User( + RealUserProfile.RealUser( profileImage = R.drawable.myimage, name = "박동민", message = "일본티비" ), - UserProfile.User( + RealUserProfile.RealUser( profileImage = R.drawable.myimage, name = "붕어빵", message = "슈크림근본" ), - UserProfile.User( + RealUserProfile.RealUser( profileImage = R.drawable.myimage, name = "붕어", message = "예?" ), - UserProfile.User( + RealUserProfile.RealUser( profileImage = R.drawable.myimage, name = "붕", message = "붕붕아" ), - UserProfile.User( + RealUserProfile.RealUser( profileImage = R.drawable.myimage, name = "시험", message = "멈춰티비" ), - UserProfile.User( + RealUserProfile.RealUser( profileImage = R.drawable.myimage, name = "숙대", message = "눈송티비" ), - UserProfile.User( + RealUserProfile.RealUser( profileImage = R.drawable.myimage, name = "새벽", message = "배고픔티비" ), - UserProfile.User( + RealUserProfile.RealUser( profileImage = R.drawable.myimage, name = "솝트", message = "안드티비" diff --git a/app/src/main/java/org/sopt/dosopttemplate/MainAdapter.kt b/app/src/main/java/org/sopt/dosopttemplate/presentation/home/MainAdapter.kt similarity index 69% rename from app/src/main/java/org/sopt/dosopttemplate/MainAdapter.kt rename to app/src/main/java/org/sopt/dosopttemplate/presentation/home/MainAdapter.kt index d64dfd9..7fba0a8 100644 --- a/app/src/main/java/org/sopt/dosopttemplate/MainAdapter.kt +++ b/app/src/main/java/org/sopt/dosopttemplate/presentation/home/MainAdapter.kt @@ -1,20 +1,24 @@ +package org.sopt.dosopttemplate.presentation.home + import android.content.Context import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView -import org.sopt.dosopttemplate.FriendViewHolder -import org.sopt.dosopttemplate.MyProfileViewHolder -import org.sopt.dosopttemplate.UserProfile +import org.sopt.dosopttemplate.presentation.home.FriendViewHolder import org.sopt.dosopttemplate.databinding.ItemFriendBinding import org.sopt.dosopttemplate.databinding.ItemMyprofileBinding +import org.sopt.dosopttemplate.presentation.home.MyProfileViewHolder +import org.sopt.dosopttemplate.presentation.home.RealUserProfile class MainAdapter(requireContext: Context) : RecyclerView.Adapter() { - lateinit var profileList: MutableList + lateinit var profileList: MutableList - private val View_Myprofile = 0 - private val View_Friend = 1 + companion object { + const val View_Myprofile = 0 + const val View_Friend = 1 + } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return when (viewType) { @@ -34,10 +38,10 @@ class MainAdapter(requireContext: Context) : val item = profileList[position] when (holder) { is MyProfileViewHolder -> { - holder.onBind(item as UserProfile.My ) + holder.onBind(item as RealUserProfile.My ) } is FriendViewHolder -> { - holder.onBind(item as UserProfile.User) + holder.onBind(item as RealUserProfile.RealUser) } } } @@ -47,8 +51,8 @@ class MainAdapter(requireContext: Context) : } override fun getItemViewType(position: Int): Int = when(profileList[position]){ - is UserProfile.My -> View_Myprofile - is UserProfile.User -> View_Friend + is RealUserProfile.My -> View_Myprofile + is RealUserProfile.RealUser -> View_Friend } } diff --git a/app/src/main/java/org/sopt/dosopttemplate/MyProfileViewHolder.kt b/app/src/main/java/org/sopt/dosopttemplate/presentation/home/MyProfileViewHolder.kt similarity index 54% rename from app/src/main/java/org/sopt/dosopttemplate/MyProfileViewHolder.kt rename to app/src/main/java/org/sopt/dosopttemplate/presentation/home/MyProfileViewHolder.kt index ae45e34..551a24a 100644 --- a/app/src/main/java/org/sopt/dosopttemplate/MyProfileViewHolder.kt +++ b/app/src/main/java/org/sopt/dosopttemplate/presentation/home/MyProfileViewHolder.kt @@ -1,20 +1,19 @@ -package org.sopt.dosopttemplate +package org.sopt.dosopttemplate.presentation.home import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide -import org.sopt.dosopttemplate.databinding.ItemFriendBinding import org.sopt.dosopttemplate.databinding.ItemMyprofileBinding class MyProfileViewHolder(private val binding: ItemMyprofileBinding) : RecyclerView.ViewHolder(binding.root) { - fun onBind(userUserProfileData: UserProfile.My) { + fun onBind(userRealUserProfileData: RealUserProfile.My) { with(binding) { Glide.with(ivProfile) - .load(userUserProfileData.profileImage) + .load(userRealUserProfileData.profileImage) .into(ivProfile) - tvName.text = userUserProfileData.name - tvSelfMessage.text = userUserProfileData.message + tvName.text = userRealUserProfileData.name + tvSelfMessage.text = userRealUserProfileData.message } } } diff --git a/app/src/main/java/org/sopt/dosopttemplate/UserProfile.kt b/app/src/main/java/org/sopt/dosopttemplate/presentation/home/RealUserProfile.kt similarity index 61% rename from app/src/main/java/org/sopt/dosopttemplate/UserProfile.kt rename to app/src/main/java/org/sopt/dosopttemplate/presentation/home/RealUserProfile.kt index 243ad2d..973544f 100644 --- a/app/src/main/java/org/sopt/dosopttemplate/UserProfile.kt +++ b/app/src/main/java/org/sopt/dosopttemplate/presentation/home/RealUserProfile.kt @@ -1,16 +1,16 @@ -package org.sopt.dosopttemplate +package org.sopt.dosopttemplate.presentation.home import androidx.annotation.DrawableRes -sealed class UserProfile { +sealed class RealUserProfile { data class My( @DrawableRes val profileImage: Int, val name: String, val message: String - ) : UserProfile() + ) : RealUserProfile() - data class User( + data class RealUser( @DrawableRes val profileImage: Int, val name: String, val message: String - ) : UserProfile() + ) : RealUserProfile() } \ No newline at end of file diff --git a/app/src/main/java/org/sopt/dosopttemplate/presentation/login/LoginActivity.kt b/app/src/main/java/org/sopt/dosopttemplate/presentation/login/LoginActivity.kt new file mode 100644 index 0000000..f9943cf --- /dev/null +++ b/app/src/main/java/org/sopt/dosopttemplate/presentation/login/LoginActivity.kt @@ -0,0 +1,73 @@ +package org.sopt.dosopttemplate.presentation.login + +import android.content.Intent +import android.os.Bundle +import android.widget.Toast +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import org.sopt.dosopttemplate.presentation.home.HomeActivity +import org.sopt.dosopttemplate.databinding.ActivityLoginBinding +import org.sopt.dosopttemplate.presentation.signup.SignUpActivity + +class LoginActivity : AppCompatActivity() { + private lateinit var binding: ActivityLoginBinding + private val loginViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityLoginBinding.inflate(layoutInflater) + setContentView(binding.root) + + binding.lifecycleOwner = this + binding.loginViewModel = loginViewModel + + initLoginBtnListener() + initSignUpBtnListener() + observeLoginResult() + } + + private fun initLoginBtnListener() { + binding.loginButton.setOnClickListener { + val id = binding.inputTextId.text.toString() + val password = binding.inputTextPw.text.toString() + + loginViewModel.login( + id = id, + password = password, + ) + } + } + + private fun initSignUpBtnListener() { + binding.signupButton.setOnClickListener { + Intent(this, SignUpActivity::class.java).apply { + startActivity(this) + } + finish() + } + } + + private fun observeLoginResult() { + loginViewModel.uiState.flowWithLifecycle(lifecycle).onEach { loginState -> + when (loginState) { + is UiState.Success -> { + Toast.makeText(this@LoginActivity, "로그인 성공", Toast.LENGTH_SHORT).show() + startActivity(Intent(this@LoginActivity, HomeActivity::class.java)) + } + + is UiState.Error -> { + Toast.makeText(this@LoginActivity, "로그인 실패", Toast.LENGTH_SHORT).show() + } + + is UiState.Loading -> { + Toast.makeText(this@LoginActivity, "로그인 중", Toast.LENGTH_SHORT).show() + } + } + + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/dosopttemplate/presentation/login/LoginViewModel.kt b/app/src/main/java/org/sopt/dosopttemplate/presentation/login/LoginViewModel.kt new file mode 100644 index 0000000..738356e --- /dev/null +++ b/app/src/main/java/org/sopt/dosopttemplate/presentation/login/LoginViewModel.kt @@ -0,0 +1,41 @@ +package org.sopt.dosopttemplate.presentation.login + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import org.sopt.dosopttemplate.data.model.request.RequestLoginDto +import org.sopt.dosopttemplate.data.model.response.ResponseLoginDto +import org.sopt.dosopttemplate.di.ServicePool.authService + +class LoginViewModel : ViewModel() { + private val _uiState = MutableStateFlow>(UiState.Loading) + val uiState: StateFlow> = _uiState.asStateFlow() + + fun login(id: String, password: String) { + viewModelScope.launch { + try { + val response = authService.postLogin(RequestLoginDto(id, password)).execute() + + if (response.isSuccessful) { + val responseBody = response.body() + if (responseBody != null) { + _uiState.value = UiState.Success(responseBody) + } else { + _uiState.value = UiState.Error + } + } else { + _uiState.value = UiState.Error + } + } catch (e: Exception) { + Log.d("LoginViewModel","실패") + _uiState.value = UiState.Error + } + } + } +} + + diff --git a/app/src/main/java/org/sopt/dosopttemplate/presentation/login/UiState.kt b/app/src/main/java/org/sopt/dosopttemplate/presentation/login/UiState.kt new file mode 100644 index 0000000..f114c7e --- /dev/null +++ b/app/src/main/java/org/sopt/dosopttemplate/presentation/login/UiState.kt @@ -0,0 +1,8 @@ +package org.sopt.dosopttemplate.presentation.login + + +sealed class UiState { + object Loading : UiState() + data class Success(val data: T) : UiState() + object Error : UiState() +} diff --git a/app/src/main/java/org/sopt/dosopttemplate/MainActivity.kt b/app/src/main/java/org/sopt/dosopttemplate/presentation/main/MainActivity.kt similarity index 95% rename from app/src/main/java/org/sopt/dosopttemplate/MainActivity.kt rename to app/src/main/java/org/sopt/dosopttemplate/presentation/main/MainActivity.kt index c863255..fb7549a 100644 --- a/app/src/main/java/org/sopt/dosopttemplate/MainActivity.kt +++ b/app/src/main/java/org/sopt/dosopttemplate/presentation/main/MainActivity.kt @@ -1,4 +1,4 @@ -package org.sopt.dosopttemplate +package org.sopt.dosopttemplate.presentation.main import androidx.appcompat.app.AppCompatActivity import android.os.Bundle diff --git a/app/src/main/java/org/sopt/dosopttemplate/MyPageFragment.kt b/app/src/main/java/org/sopt/dosopttemplate/presentation/mypage/MyPageFragment.kt similarity index 96% rename from app/src/main/java/org/sopt/dosopttemplate/MyPageFragment.kt rename to app/src/main/java/org/sopt/dosopttemplate/presentation/mypage/MyPageFragment.kt index 85f6a4a..49780df 100644 --- a/app/src/main/java/org/sopt/dosopttemplate/MyPageFragment.kt +++ b/app/src/main/java/org/sopt/dosopttemplate/presentation/mypage/MyPageFragment.kt @@ -1,7 +1,6 @@ -package org.sopt.dosopttemplate +package org.sopt.dosopttemplate.presentation.mypage import android.os.Bundle -import android.util.Log import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View diff --git a/app/src/main/java/org/sopt/dosopttemplate/presentation/signup/SignUpActivity.kt b/app/src/main/java/org/sopt/dosopttemplate/presentation/signup/SignUpActivity.kt new file mode 100644 index 0000000..b5999d6 --- /dev/null +++ b/app/src/main/java/org/sopt/dosopttemplate/presentation/signup/SignUpActivity.kt @@ -0,0 +1,61 @@ +package org.sopt.dosopttemplate.presentation.signup + +import android.content.Intent +import android.os.Bundle +import android.widget.Toast +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import org.sopt.dosopttemplate.R +import org.sopt.dosopttemplate.databinding.ActivitySignupBinding +import org.sopt.dosopttemplate.presentation.login.LoginActivity + +class SignUpActivity : AppCompatActivity() { + private lateinit var binding: ActivitySignupBinding + private val signupViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivitySignupBinding.inflate(layoutInflater) + setContentView(binding.root) + + binding.lifecycleOwner = this + binding.signupViewModel = signupViewModel + + initSignUpBtnListener() + observeSignupResult() + } + + private var signUpId: String = "" + private var signUpPassword: String = "" + private var signUpMajor: String = "" + private var signUpName: String = "" + public fun initSignUpBtnListener() { + binding.signupButton.setOnClickListener { + val signUpId = binding.editTextId.text.toString() + val signUpPassword = binding.editTextPassword.text.toString() + val signUpMajor = binding.editTextMajor.text.toString() + val signUpName = binding.editTextName.text.toString() + + signupViewModel.signup(signUpId, signUpPassword, signUpMajor, signUpName) + + } + } + + private fun observeSignupResult() { + signupViewModel.signupSuccess.observe(this) { isSuccess -> + if (isSuccess) { + Intent(this, LoginActivity::class.java).apply { + putExtra("ID", signUpId) + putExtra("PASSWORD", signUpPassword) + putExtra("MAJOR", signUpMajor) + putExtra("NAME", signUpName) + startActivity(this) + } + finish() + } else { + Toast.makeText(this@SignUpActivity, getString(R.string.signup_failed), Toast.LENGTH_SHORT).show() + + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/dosopttemplate/presentation/signup/SignupViewModel.kt b/app/src/main/java/org/sopt/dosopttemplate/presentation/signup/SignupViewModel.kt new file mode 100644 index 0000000..a42db6e --- /dev/null +++ b/app/src/main/java/org/sopt/dosopttemplate/presentation/signup/SignupViewModel.kt @@ -0,0 +1,34 @@ +package org.sopt.dosopttemplate.presentation.signup + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.sopt.dosopttemplate.data.model.request.RequestSignupDto +import org.sopt.dosopttemplate.di.ServicePool +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + +class SignupViewModel : ViewModel() { + private val _signupSuccess: MutableLiveData = MutableLiveData() + val signupSuccess = _signupSuccess + + private val _isSignupBtn = MutableLiveData(false) + val isSignupBtn = _isSignupBtn + + val id = MutableLiveData("") + val password = MutableLiveData("") + val major = MutableLiveData("") + val name = MutableLiveData("") + + fun signup(id: String, password: String, major: String, name: String) { + ServicePool.authService.postSignUp(RequestSignupDto(id, password, major, name)) + .enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + _signupSuccess.value = response.isSuccessful + } + + override fun onFailure(call: Call, t: Throwable) { + } + }) + } +} diff --git a/app/src/main/java/org/sopt/dosopttemplate/presentation/user/UserAdapter.kt b/app/src/main/java/org/sopt/dosopttemplate/presentation/user/UserAdapter.kt new file mode 100644 index 0000000..17c9a29 --- /dev/null +++ b/app/src/main/java/org/sopt/dosopttemplate/presentation/user/UserAdapter.kt @@ -0,0 +1,30 @@ +package org.sopt.dosopttemplate.presentation.user + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import org.sopt.dosopttemplate.databinding.ItemUserBinding +import org.sopt.dosopttemplate.domain.entity.UserEntity + +class UserAdapter : RecyclerView.Adapter() { + private val userList = mutableListOf() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder { + val binding = ItemUserBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return UserViewHolder(binding) + } + + override fun onBindViewHolder(holder: UserViewHolder, position: Int) { + holder.onBind(userList[position]) + } + + override fun getItemCount(): Int { + return userList.size + } + + fun setUserList(userData: List) { + userList.clear() + userList.addAll(userData) + notifyDataSetChanged() + } +} diff --git a/app/src/main/java/org/sopt/dosopttemplate/presentation/user/UserFragment.kt b/app/src/main/java/org/sopt/dosopttemplate/presentation/user/UserFragment.kt new file mode 100644 index 0000000..0d03c33 --- /dev/null +++ b/app/src/main/java/org/sopt/dosopttemplate/presentation/user/UserFragment.kt @@ -0,0 +1,75 @@ +package org.sopt.dosopttemplate.presentation.user + +import androidx.fragment.app.Fragment +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.fragment.app.viewModels +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import org.sopt.dosopttemplate.R +import org.sopt.dosopttemplate.data.model.response.ResponseUserDto +import org.sopt.dosopttemplate.databinding.FragmentUserBinding +import org.sopt.dosopttemplate.di.ServicePool +import org.sopt.dosopttemplate.domain.entity.UserEntity +import org.sopt.dosopttemplate.presentation.login.UiState +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + +@AndroidEntryPoint +class UserFragment : Fragment() { + private var _binding: FragmentUserBinding? = null + private val binding: FragmentUserBinding + get() = _binding ?: throw IllegalStateException("바인딩 초기화 안됐어유") + private val UserViewModel by viewModels() + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? + ): View { + _binding = FragmentUserBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + connectAdapter() + observeUserState() + UserViewModel.getUserFromServer(2) + } + + private lateinit var userAdapter: UserAdapter + private fun connectAdapter() { + userAdapter = UserAdapter() + binding.rvUser.adapter = userAdapter + } + + private fun observeUserState() { + UserViewModel.userState.flowWithLifecycle(lifecycle).onEach { userState -> + when (userState) { + is UiState.Success -> { + getUser(userState.data) + } + is UiState.Error -> { + Toast.makeText(requireContext(), "서버 에러", Toast.LENGTH_SHORT).show() + } + is UiState.Loading -> { + Toast.makeText(requireContext(), "로딩중인데염", Toast.LENGTH_SHORT).show() + } + } + }.launchIn(lifecycleScope) + } + + private fun getUser(userList: List) { + userAdapter.setUserList(userList) + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/dosopttemplate/presentation/user/UserViewHolder.kt b/app/src/main/java/org/sopt/dosopttemplate/presentation/user/UserViewHolder.kt new file mode 100644 index 0000000..67740f3 --- /dev/null +++ b/app/src/main/java/org/sopt/dosopttemplate/presentation/user/UserViewHolder.kt @@ -0,0 +1,21 @@ +package org.sopt.dosopttemplate.presentation.user + +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import org.sopt.dosopttemplate.databinding.ItemUserBinding +import org.sopt.dosopttemplate.domain.entity.UserEntity + + +class UserViewHolder( + private val binding: ItemUserBinding +): RecyclerView.ViewHolder(binding.root) { + + fun onBind(item: UserEntity) { + binding.tvUserName.text = item.firstName + binding.tvUserEmail.text = item.email + Glide.with(binding.root.context) + .load(item.avatar) + .into(binding.ivUserAvatar) + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/dosopttemplate/presentation/user/UserViewModel.kt b/app/src/main/java/org/sopt/dosopttemplate/presentation/user/UserViewModel.kt new file mode 100644 index 0000000..3b9a297 --- /dev/null +++ b/app/src/main/java/org/sopt/dosopttemplate/presentation/user/UserViewModel.kt @@ -0,0 +1,40 @@ +package org.sopt.dosopttemplate.presentation.user + +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.dosopttemplate.domain.entity.UserEntity +import org.sopt.dosopttemplate.domain.repository.UserRepository +import org.sopt.dosopttemplate.presentation.login.UiState +import javax.inject.Inject + +@HiltViewModel +class UserViewModel @Inject constructor( + private val repository: UserRepository, +) : ViewModel() { + private val _userState = MutableStateFlow>>(UiState.Loading) + val userState: StateFlow>> get() = _userState + + fun getUserFromServer(page: Int) { + _userState.value = UiState.Loading + viewModelScope.launch { + repository.getUserList(page) + .onSuccess { userList -> + val User = userList.map { userEntity -> + UserEntity( + avatar = userEntity.avatar, + email = userEntity.email, + firstName = userEntity.firstName, + ) + } + _userState.value = UiState.Success(User) + } + .onFailure { exception -> + _userState.value = UiState.Error + } + } + } +} diff --git a/app/src/main/res/drawable/ic_fab_capture_pink_24.xml b/app/src/main/res/drawable/ic_fab_capture_pink_24.xml new file mode 100644 index 0000000..c3517f1 --- /dev/null +++ b/app/src/main/res/drawable/ic_fab_capture_pink_24.xml @@ -0,0 +1,6 @@ + + + + diff --git a/app/src/main/res/drawable/ic_fab_main_pink_24.xml b/app/src/main/res/drawable/ic_fab_main_pink_24.xml new file mode 100644 index 0000000..afef92e --- /dev/null +++ b/app/src/main/res/drawable/ic_fab_main_pink_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_fab_share_pink_24.xml b/app/src/main/res/drawable/ic_fab_share_pink_24.xml new file mode 100644 index 0000000..071b829 --- /dev/null +++ b/app/src/main/res/drawable/ic_fab_share_pink_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/menu_selector_color.xml b/app/src/main/res/drawable/menu_selector_color.xml new file mode 100644 index 0000000..bef68a6 --- /dev/null +++ b/app/src/main/res/drawable/menu_selector_color.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/font/do_sopt_font.xml b/app/src/main/res/font/do_sopt_font.xml new file mode 100644 index 0000000..891a76f --- /dev/null +++ b/app/src/main/res/font/do_sopt_font.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_home.xml b/app/src/main/res/layout/activity_home.xml index 52aca85..29843fd 100644 --- a/app/src/main/res/layout/activity_home.xml +++ b/app/src/main/res/layout/activity_home.xml @@ -4,7 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".HomeActivity"> + tools:context=".presentation.home.HomeActivity"> + + + + + + + + app:menu="@menu/menu_home" + app:itemIconTint="@drawable/menu_selector_color" + app:itemTextColor="@drawable/menu_selector_color"/> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index 6c65fc2..372e68a 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -1,12 +1,20 @@ - + + + + + + + tools:context=".presentation.login.LoginActivity"> - \ No newline at end of file + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 5934347..3ef5ac7 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -6,7 +6,7 @@ android:layout_height="match_parent" android:paddingHorizontal="60dp" android:paddingVertical="60dp" - tools:context=".MainActivity"> + tools:context=".presentation.main.MainActivity"> - + + + + + + + tools:context=".presentation.signup.SignUpActivity"> + app:layout_constraintTop_toTopOf="@id/tv_signup_title" /> - + app:layout_constraintStart_toStartOf="@id/tv_signup_id" + app:layout_constraintTop_toBottomOf="@id/tv_signup_id"> + + + - + + + app:layout_constraintHorizontal_bias="1.0" + app:layout_constraintStart_toStartOf="@id/tv_signup_pw" + app:layout_constraintTop_toBottomOf="@id/tv_signup_pw"> + + + - + + app:layout_constraintHorizontal_bias="1.0" + app:layout_constraintStart_toStartOf="@id/tv_signup_major" + app:layout_constraintTop_toBottomOf="@id/tv_signup_major"> + + + - + + app:layout_constraintHorizontal_bias="1.0" + app:layout_constraintStart_toStartOf="@id/tv_signup_name" + app:layout_constraintTop_toBottomOf="@id/tv_signup_name"> + + +