프로필 수정 페이지 추가

This commit is contained in:
2023-08-18 19:39:04 +09:00
parent 9adadaf572
commit be7c7d0682
25 changed files with 2156 additions and 2 deletions

View File

@@ -55,6 +55,11 @@ import kr.co.vividnext.sodalive.mypage.can.CanRepository
import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeViewModel
import kr.co.vividnext.sodalive.mypage.can.payment.CanPaymentViewModel
import kr.co.vividnext.sodalive.mypage.can.status.CanStatusViewModel
import kr.co.vividnext.sodalive.mypage.profile.ProfileUpdateViewModel
import kr.co.vividnext.sodalive.mypage.profile.nickname.NicknameUpdateViewModel
import kr.co.vividnext.sodalive.mypage.profile.tag.MemberTagApi
import kr.co.vividnext.sodalive.mypage.profile.tag.MemberTagRepository
import kr.co.vividnext.sodalive.mypage.profile.tag.MemberTagViewModel
import kr.co.vividnext.sodalive.mypage.service_center.FaqApi
import kr.co.vividnext.sodalive.mypage.service_center.FaqRepository
import kr.co.vividnext.sodalive.mypage.service_center.ServiceCenterViewModel
@@ -134,6 +139,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
single { ApiBuilder().build(get(), NoticeApi::class.java) }
single { ApiBuilder().build(get(), AudioContentApi::class.java) }
single { ApiBuilder().build(get(), FaqApi::class.java) }
single { ApiBuilder().build(get(), MemberTagApi::class.java) }
}
private val viewModelModule = module {
@@ -179,6 +185,9 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
viewModel { AudioContentCommentReplyViewModel(get()) }
viewModel { FollowingCreatorViewModel(get()) }
viewModel { ServiceCenterViewModel(get()) }
viewModel { ProfileUpdateViewModel(get()) }
viewModel { NicknameUpdateViewModel(get()) }
viewModel { MemberTagViewModel(get()) }
}
private val repositoryModule = module {
@@ -199,6 +208,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
factory { PlaybackTrackingRepository(get()) }
factory { FollowingCreatorRepository(get(), get()) }
factory { FaqRepository(get()) }
factory { MemberTagRepository(get()) }
}
private val moduleList = listOf(

View File

@@ -19,12 +19,13 @@ import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.FragmentMyBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import kr.co.vividnext.sodalive.extensions.moneyFormat
import kr.co.vividnext.sodalive.live.reservation_status.LiveReservationStatusActivity
import kr.co.vividnext.sodalive.mypage.auth.Auth
import kr.co.vividnext.sodalive.mypage.auth.AuthVerifyRequest
import kr.co.vividnext.sodalive.mypage.auth.BootpayResponse
import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeActivity
import kr.co.vividnext.sodalive.mypage.can.status.CanStatusActivity
import kr.co.vividnext.sodalive.live.reservation_status.LiveReservationStatusActivity
import kr.co.vividnext.sodalive.mypage.profile.ProfileUpdateActivity
import kr.co.vividnext.sodalive.mypage.service_center.ServiceCenterActivity
import kr.co.vividnext.sodalive.settings.SettingsActivity
import kr.co.vividnext.sodalive.settings.notification.MemberRole
@@ -59,7 +60,14 @@ class MyPageFragment : BaseFragment<FragmentMyBinding>(FragmentMyBinding::inflat
)
}
binding.ivEdit.setOnClickListener {}
binding.ivEdit.setOnClickListener {
startActivity(
Intent(
requireActivity(),
ProfileUpdateActivity::class.java
)
)
}
binding.llTotalCan.setOnClickListener {
startActivity(

View File

@@ -0,0 +1,20 @@
package kr.co.vividnext.sodalive.mypage.profile
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.user.Gender
data class ProfileResponse(
@SerializedName("userId") val userId: Long,
@SerializedName("email") val email: String,
@SerializedName("nickname") val nickname: String,
@SerializedName("gender") val gender: Gender,
@SerializedName("profileUrl") val profileUrl: String,
@SerializedName("chargeCoin") val chargeCoin: Int,
@SerializedName("rewardCoin") val rewardCoin: Int,
@SerializedName("youtubeUrl") val youtubeUrl: String?,
@SerializedName("instagramUrl") val instagramUrl: String?,
@SerializedName("blogUrl") val blogUrl: String?,
@SerializedName("websiteUrl") val websiteUrl: String?,
@SerializedName("introduce") val introduce: String,
@SerializedName("tags") val tags: List<String>
)

View File

@@ -0,0 +1,277 @@
package kr.co.vividnext.sodalive.mypage.profile
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.LinearLayout
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import coil.load
import coil.transform.CircleCropTransformation
import com.github.dhaval2404.imagepicker.ImagePicker
import com.jakewharton.rxbinding4.widget.textChanges
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.RealPathUtil
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ActivityProfileUpdateBinding
import kr.co.vividnext.sodalive.databinding.ItemLiveTagSelectedBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.mypage.profile.nickname.NicknameUpdateActivity
import kr.co.vividnext.sodalive.mypage.profile.password.ModifyPasswordActivity
import kr.co.vividnext.sodalive.mypage.profile.tag.MemberTagFragment
import kr.co.vividnext.sodalive.user.Gender
import org.koin.android.ext.android.inject
import java.util.concurrent.TimeUnit
class ProfileUpdateActivity : BaseActivity<ActivityProfileUpdateBinding>(
ActivityProfileUpdateBinding::inflate
) {
private val viewModel: ProfileUpdateViewModel by inject()
private val imageResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val resultCode = result.resultCode
val data = result.data
if (resultCode == RESULT_OK) {
// Image Uri will not be null for RESULT_OK
val fileUri = data?.data!!
binding.ivProfile.background = null
viewModel.updateProfileImage(fileUri) {
binding.ivProfile.load(it) {
crossfade(true)
transformations(CircleCropTransformation())
}
}
} else if (resultCode == ImagePicker.RESULT_ERROR) {
Toast.makeText(this, ImagePicker.getError(data), Toast.LENGTH_SHORT).show()
}
}
private val tagFragment: MemberTagFragment by lazy {
MemberTagFragment(viewModel.tags) { tag, isChecked ->
when {
isChecked -> {
viewModel.addTag(tag)
return@MemberTagFragment true
}
!isChecked -> {
viewModel.removeTag(tag)
return@MemberTagFragment true
}
else -> {
return@MemberTagFragment false
}
}
}
}
private lateinit var loadingDialog: LoadingDialog
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.getRealPathFromURI = {
RealPathUtil.getRealPath(applicationContext, it)
}
bindData()
}
override fun onStart() {
super.onStart()
viewModel.getUserInfo()
}
private fun bindData() {
compositeDisposable.add(
binding.etBlog.textChanges().skip(1)
.debounce(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe {
viewModel.blogUrl = it.toString()
}
)
compositeDisposable.add(
binding.etWebsite.textChanges().skip(1)
.debounce(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe {
viewModel.websiteUrl = it.toString()
}
)
compositeDisposable.add(
binding.etYoutube.textChanges().skip(1)
.debounce(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe {
viewModel.youtubeUrl = it.toString()
}
)
compositeDisposable.add(
binding.etInstagram.textChanges().skip(1)
.debounce(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe {
viewModel.instagramUrl = it.toString()
}
)
viewModel.genderLiveData.observe(this) {
binding.tvMale.isSelected = false
binding.tvFemale.isSelected = false
binding.tvNone.isSelected = false
when (it) {
Gender.MALE -> binding.tvMale.isSelected = true
Gender.FEMALE -> binding.tvFemale.isSelected = true
Gender.NONE -> binding.tvNone.isSelected = true
else -> {
}
}
}
viewModel.userInfoLiveData.observe(this) {
binding.ivProfile.load(it.profileUrl) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(CircleCropTransformation())
}
binding.tvEmail.text = it.email
binding.tvNickname.text = it.nickname
it.youtubeUrl?.let { url -> binding.etYoutube.setText(url) }
it.instagramUrl?.let { url -> binding.etInstagram.setText(url) }
it.websiteUrl?.let { url -> binding.etWebsite.setText(url) }
it.blogUrl?.let { url -> binding.etBlog.setText(url) }
binding.etIntroduce.setText(it.introduce)
SharedPreferenceManager.nickname = it.nickname
}
viewModel.toastLiveData.observe(this) {
it?.let {
Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show()
}
}
viewModel.isLoading.observe(this) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
viewModel.selectedTagLiveData.observe(this) {
binding.llSelectTags.removeAllViews()
binding.llSelectTags.visibility = if (it.isNotEmpty()) {
View.VISIBLE
} else {
View.GONE
}
for (index in it.indices) {
val tag = it[index]
val itemView = ItemLiveTagSelectedBinding.inflate(layoutInflater)
itemView.tvTag.text = tag
itemView.ivRemove.setOnClickListener {
viewModel.removeTag(tag)
}
binding.llSelectTags.addView(itemView.root)
if (index > 0) {
val layoutParams = itemView.root.layoutParams as LinearLayout.LayoutParams
layoutParams.marginStart = 10.dpToPx().toInt()
itemView.root.layoutParams = layoutParams
}
}
}
}
override fun setupView() {
binding.toolbar.tvBack.text = "프로필 수정"
binding.toolbar.tvBack.setOnClickListener { finish() }
loadingDialog = LoadingDialog(this, layoutInflater)
compositeDisposable.add(
binding.etIntroduce.textChanges()
.debounce(500, TimeUnit.MILLISECONDS)
.subscribeOn(Schedulers.io())
.subscribe {
viewModel.introduce = it.toString()
}
)
binding.tvMale.setOnClickListener {
viewModel.changeGender(Gender.MALE)
}
binding.tvFemale.setOnClickListener {
viewModel.changeGender(Gender.FEMALE)
}
binding.tvNone.setOnClickListener {
viewModel.changeGender(Gender.NONE)
}
binding.ivPhotoPicker.setOnClickListener {
ImagePicker.with(this)
.crop()
.galleryOnly()
.galleryMimeTypes( // Exclude gif images
mimeTypes = arrayOf(
"image/png",
"image/jpg",
"image/jpeg"
)
)
.createIntent { imageResult.launch(it) }
}
binding.tvSelectTag.setOnClickListener {
if (tagFragment.isAdded) return@setOnClickListener
tagFragment.show(supportFragmentManager, tagFragment.tag)
}
binding.tvModifyPassword.setOnClickListener {
startActivity(
Intent(
applicationContext,
ModifyPasswordActivity::class.java
)
)
}
binding.tvSave.setOnClickListener {
viewModel.updateProfile {
finish()
}
}
binding.tvChangeNickname.setOnClickListener {
startActivity(
Intent(
applicationContext,
NicknameUpdateActivity::class.java
)
)
}
}
}

View File

@@ -0,0 +1,20 @@
package kr.co.vividnext.sodalive.mypage.profile
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.user.Gender
data class ProfileUpdateRequest(
@SerializedName("email") val email: String,
@SerializedName("password") val password: String? = null,
@SerializedName("modifyPassword") val modifyPassword: String? = null,
@SerializedName("nickname") val nickname: String? = null,
@SerializedName("gender") val gender: Gender? = null,
@SerializedName("insertTags") val insertTags: List<String>? = null,
@SerializedName("removeTags") val removeTags: List<String>? = null,
@SerializedName("introduce") val introduce: String? = null,
@SerializedName("youtubeUrl") val youtubeUrl: String? = null,
@SerializedName("instagramUrl") val instagramUrl: String? = null,
@SerializedName("websiteUrl") val websiteUrl: String? = null,
@SerializedName("blogUrl") val blogUrl: String? = null,
@SerializedName("container") val container: String = "aos"
)

View File

@@ -0,0 +1,290 @@
package kr.co.vividnext.sodalive.mypage.profile
import android.net.Uri
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.user.Gender
import kr.co.vividnext.sodalive.user.UserRepository
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import java.io.File
class ProfileUpdateViewModel(private val repository: UserRepository) : BaseViewModel() {
var youtubeUrl = ""
var instagramUrl = ""
var websiteUrl = ""
var blogUrl = ""
var introduce = ""
var currentPassword = ""
var newPassword = ""
var newPasswordConfirm = ""
val tags = mutableSetOf<String>()
private val insertTags = mutableListOf<String>()
private val removeTags = mutableListOf<String>()
private lateinit var profileResponse: ProfileResponse
private val _userInfoLiveData = MutableLiveData<ProfileResponse>()
val userInfoLiveData: LiveData<ProfileResponse>
get() = _userInfoLiveData
private val _genderLiveData = MutableLiveData<Gender>()
val genderLiveData: LiveData<Gender>
get() = _genderLiveData
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private val _selectedTagLiveData = MutableLiveData<List<String>>()
val selectedTagLiveData: LiveData<List<String>>
get() = _selectedTagLiveData
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
get() = _isLoading
lateinit var getRealPathFromURI: (Uri) -> String?
fun getUserInfo() {
compositeDisposable.add(
repository.getProfile("Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
profileResponse = it.data
tags.addAll(profileResponse.tags)
_selectedTagLiveData.postValue(profileResponse.tags)
_genderLiveData.postValue(profileResponse.gender)
_userInfoLiveData.postValue(profileResponse)
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun changeGender(gender: Gender) {
_genderLiveData.postValue(gender)
}
fun updateProfileImage(uri: Uri, onSuccess: (String) -> Unit) {
val file = File(getRealPathFromURI(uri))
val image = MultipartBody.Part.createFormData(
"image",
file.name,
file.asRequestBody("image/*".toMediaType())
)
compositeDisposable.add(
repository.updateProfileImage(image, "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
onSuccess(it.data)
_toastLiveData.postValue("프로필 이미지가 변경되었습니다.")
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun updateProfile(onSuccess: () -> Unit) {
if (
profileResponse.youtubeUrl != youtubeUrl ||
profileResponse.instagramUrl != instagramUrl ||
profileResponse.blogUrl != blogUrl ||
profileResponse.websiteUrl != websiteUrl ||
profileResponse.gender != _genderLiveData.value ||
insertTags.isNotEmpty() ||
removeTags.isNotEmpty() ||
profileResponse.introduce != introduce
) {
val request = ProfileUpdateRequest(
email = profileResponse.email,
nickname = null,
youtubeUrl = if (profileResponse.youtubeUrl != youtubeUrl) {
youtubeUrl
} else {
null
},
instagramUrl = if (profileResponse.instagramUrl != instagramUrl) {
instagramUrl
} else {
null
},
blogUrl = if (profileResponse.blogUrl != blogUrl) {
blogUrl
} else {
null
},
websiteUrl = if (profileResponse.websiteUrl != websiteUrl) {
websiteUrl
} else {
null
},
gender = if (profileResponse.gender != _genderLiveData.value) {
_genderLiveData.value
} else {
null
},
introduce = if (profileResponse.introduce != introduce) {
introduce
} else {
null
},
insertTags = insertTags.ifEmpty { null },
removeTags = removeTags.ifEmpty { null }
)
_isLoading.value = true
compositeDisposable.add(
repository.updateProfile(request, "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success) {
_toastLiveData.postValue(
"프로필이 변경되었습니다."
)
onSuccess()
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
} else run {
onSuccess()
}
}
fun updatePassword(onSuccess: () -> Unit) {
val email = SharedPreferenceManager.email
if (currentPassword.isBlank()) {
_toastLiveData.postValue("현재 비밀번호를 입력하세요")
return
}
if (newPassword.isBlank()) {
_toastLiveData.postValue("변경할 비밀번호를 입력하세요")
return
}
if (newPasswordConfirm != newPassword) {
_toastLiveData.postValue("비밀번호가 일치하지 않습니다.")
return
}
val request = ProfileUpdateRequest(
email = email,
password = currentPassword,
modifyPassword = newPassword
)
_isLoading.value = true
compositeDisposable.add(
repository.updateProfile(request, "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success) {
_toastLiveData.postValue(
"비밀번호가 변경되었습니다."
)
onSuccess()
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun removeTag(tag: String) {
tags.remove(tag)
if (insertTags.contains(tag)) {
insertTags.remove(tag)
} else {
removeTags.add(tag)
}
_selectedTagLiveData.postValue(tags.toList())
}
fun addTag(tag: String) {
tags.add(tag)
if (removeTags.contains(tag)) {
removeTags.remove(tag)
} else {
insertTags.add(tag)
}
_selectedTagLiveData.postValue(tags.toList())
}
}

View File

@@ -0,0 +1,5 @@
package kr.co.vividnext.sodalive.mypage.profile.nickname
import com.google.gson.annotations.SerializedName
data class GetChangeNicknamePriceResponse(@SerializedName("price") val price: Int)

View File

@@ -0,0 +1,83 @@
package kr.co.vividnext.sodalive.mypage.profile.nickname
import android.annotation.SuppressLint
import android.os.Bundle
import android.widget.Toast
import com.jakewharton.rxbinding4.widget.textChanges
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ActivityNicknameUpdateBinding
import org.koin.android.ext.android.inject
import java.util.concurrent.TimeUnit
class NicknameUpdateActivity : BaseActivity<ActivityNicknameUpdateBinding>(
ActivityNicknameUpdateBinding::inflate
) {
private val viewModel: NicknameUpdateViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bindData()
viewModel.getChangeNicknamePrice()
binding.etNickname.setText(SharedPreferenceManager.nickname)
}
override fun setupView() {
binding.toolbar.tvBack.text = "닉네임 변경"
binding.toolbar.tvBack.setOnClickListener { finish() }
loadingDialog = LoadingDialog(this, layoutInflater)
binding.tvCheckNickname.setOnClickListener {
viewModel.checkNickname()
}
binding.tvChangeNickname.setOnClickListener {
viewModel.changeNickname { finish() }
}
}
@SuppressLint("SetTextI18n")
private fun bindData() {
compositeDisposable.add(
binding.etNickname.textChanges().skip(1)
.debounce(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe {
viewModel.isCheckedNickname = false
viewModel.nickname = it.toString()
}
)
viewModel.toastLiveData.observe(this) {
it?.let {
Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show()
}
}
viewModel.isLoading.observe(this) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
viewModel.priceLiveData.observe(this) {
if (it > 0) {
binding.tvChangeNickname.text = "${it}코인으로 닉네임 변경하기"
} else {
binding.tvChangeNickname.text = "닉네임 변경하기"
}
}
}
}

View File

@@ -0,0 +1,142 @@
package kr.co.vividnext.sodalive.mypage.profile.nickname
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.mypage.profile.ProfileUpdateRequest
import kr.co.vividnext.sodalive.user.UserRepository
class NicknameUpdateViewModel(private val repository: UserRepository) : BaseViewModel() {
var nickname = ""
private val _priceLiveData = MutableLiveData(0)
val priceLiveData: LiveData<Int>
get() = _priceLiveData
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
get() = _isLoading
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
var isCheckedNickname = false
fun getChangeNicknamePrice() {
_isLoading.value = true
compositeDisposable.add(
repository.getChangeNicknamePrice(token = "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success && it.data != null) {
_priceLiveData.value = it.data.price
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun checkNickname() {
if (nickname.isNotBlank()) {
_isLoading.value = true
compositeDisposable.add(
repository.checkNickname(nickname)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success) {
isCheckedNickname = true
_toastLiveData.postValue("사용가능한 닉네임 입니다.")
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
)
)
} else {
_toastLiveData.postValue("닉네임을 입력하세요.")
}
}
fun changeNickname(onSuccess: () -> Unit) {
if (isCheckedNickname) {
_isLoading.value = true
compositeDisposable.add(
repository.changeNickname(
request = ProfileUpdateRequest(
email = SharedPreferenceManager.email,
nickname = nickname
),
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success) {
_toastLiveData.postValue("닉네임이 변경되었습니다.")
SharedPreferenceManager.nickname = nickname
onSuccess()
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
)
)
} else {
_toastLiveData.postValue("닉네임 중복체크를 해주세요.")
}
}
}

View File

@@ -0,0 +1,72 @@
package kr.co.vividnext.sodalive.mypage.profile.password
import android.os.Bundle
import android.widget.Toast
import com.jakewharton.rxbinding4.widget.textChanges
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.databinding.ActivityModifyPasswordBinding
import kr.co.vividnext.sodalive.mypage.profile.ProfileUpdateViewModel
import org.koin.android.ext.android.inject
import java.util.concurrent.TimeUnit
class ModifyPasswordActivity : BaseActivity<ActivityModifyPasswordBinding>(
ActivityModifyPasswordBinding::inflate
) {
private val viewModel: ProfileUpdateViewModel by inject()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bindData()
}
private fun bindData() {
compositeDisposable.add(
binding.etCurrentPassword.textChanges().skip(1)
.debounce(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe {
viewModel.currentPassword = it.toString()
}
)
compositeDisposable.add(
binding.etNewPassword.textChanges().skip(1)
.debounce(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe {
viewModel.newPassword = it.toString()
}
)
compositeDisposable.add(
binding.etNewPasswordConfirm.textChanges().skip(1)
.debounce(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe {
viewModel.newPasswordConfirm = it.toString()
}
)
viewModel.toastLiveData.observe(this) {
it?.let {
Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show()
}
}
}
override fun setupView() {
binding.toolbar.tvBack.text = "비밀번호 변경"
binding.toolbar.tvBack.setOnClickListener { finish() }
binding.tvModifyPassword.setOnClickListener {
viewModel.updatePassword { finish() }
}
}
}

View File

@@ -0,0 +1,76 @@
package kr.co.vividnext.sodalive.mypage.profile.tag
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.RoundedCornersTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemLiveTagBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class MemberTagAdapter(
private val selectedTags: Set<String>,
private val onItemClick: (String, Boolean) -> Boolean
) : RecyclerView.Adapter<MemberTagAdapter.ViewHolder>() {
inner class ViewHolder(
private val binding: ItemLiveTagBinding
) : RecyclerView.ViewHolder(binding.root) {
private var isChecked = false
fun bind(item: MemberTagResponse) {
if (selectedTags.contains(item.tag)) {
binding.ivTagChecked.visibility = View.VISIBLE
binding.ivTag.setBackgroundResource(R.drawable.bg_round_corner_30_9970ff)
isChecked = true
} else {
binding.ivTagChecked.visibility = View.GONE
binding.ivTag.background = null
isChecked = false
}
binding.ivTag.load(item.image) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(30f.dpToPx()))
}
binding.tvTag.text = item.tag
binding.root.setOnClickListener {
isChecked = !isChecked
if (onItemClick(item.tag, isChecked)) {
if (isChecked) {
binding.ivTagChecked.visibility = View.VISIBLE
binding.ivTag.setBackgroundResource(R.drawable.bg_round_corner_30_9970ff)
} else {
binding.ivTagChecked.visibility = View.GONE
binding.ivTag.background = null
}
} else {
isChecked = !isChecked
}
}
}
}
val items = mutableSetOf<MemberTagResponse>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
ItemLiveTagBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items.toList()[position])
}
override fun getItemCount() = items.size
}

View File

@@ -0,0 +1,13 @@
package kr.co.vividnext.sodalive.mypage.profile.tag
import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.common.ApiResponse
import retrofit2.http.GET
import retrofit2.http.Header
interface MemberTagApi {
@GET("/member/tag")
fun getTags(
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<MemberTagResponse>>>
}

View File

@@ -0,0 +1,117 @@
package kr.co.vividnext.sodalive.mypage.profile.tag
import android.annotation.SuppressLint
import android.app.Dialog
import android.graphics.Rect
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
class MemberTagFragment(
private val selectedTags: Set<String>,
private val onItemClick: (String, Boolean) -> Boolean
) : BottomSheetDialogFragment() {
private val viewModel: MemberTagViewModel by inject()
private lateinit var adapter: MemberTagAdapter
private lateinit var loadingDialog: LoadingDialog
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState)
dialog.setOnShowListener {
val d = it as BottomSheetDialog
val bottomSheet = d.findViewById<FrameLayout>(
com.google.android.material.R.id.design_bottom_sheet
)
if (bottomSheet != null) {
BottomSheetBehavior.from(bottomSheet).state = BottomSheetBehavior.STATE_EXPANDED
}
}
return dialog
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View = inflater.inflate(R.layout.fragment_creator_tag, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
view.findViewById<ImageView>(R.id.iv_close).setOnClickListener {
dialog?.dismiss()
}
view.findViewById<TextView>(R.id.tv_select).setOnClickListener {
dialog?.dismiss()
}
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
setupAdapter(view)
bindData()
viewModel.getTags()
}
private fun setupAdapter(view: View) {
val recyclerView = view.findViewById<RecyclerView>(R.id.rv_tags)
adapter = MemberTagAdapter(selectedTags) { tag, isChecked ->
return@MemberTagAdapter onItemClick(tag, isChecked)
}
recyclerView.setHasFixedSize(true)
recyclerView.layoutManager = GridLayoutManager(requireContext(), 4)
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
outRect.top = 13.3f.dpToPx().toInt()
outRect.bottom = 13.3f.dpToPx().toInt()
outRect.left = 13.3f.dpToPx().toInt()
outRect.right = 13.3f.dpToPx().toInt()
}
})
recyclerView.adapter = adapter
}
@SuppressLint("NotifyDataSetChanged")
private fun bindData() {
viewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireActivity(), it, Toast.LENGTH_LONG).show() }
}
viewModel.tagLiveData.observe(viewLifecycleOwner) {
adapter.items.addAll(it)
adapter.notifyDataSetChanged()
}
viewModel.isLoading.observe(viewLifecycleOwner) {
if (it) {
loadingDialog.show(resources.displayMetrics.widthPixels)
} else {
loadingDialog.dismiss()
}
}
}
}

View File

@@ -0,0 +1,5 @@
package kr.co.vividnext.sodalive.mypage.profile.tag
class MemberTagRepository(private val api: MemberTagApi) {
fun getTags(token: String) = api.getTags(authHeader = token)
}

View File

@@ -0,0 +1,9 @@
package kr.co.vividnext.sodalive.mypage.profile.tag
import com.google.gson.annotations.SerializedName
data class MemberTagResponse(
@SerializedName("id") val id: Long,
@SerializedName("tag") val tag: String,
@SerializedName("image") val image: String
)

View File

@@ -0,0 +1,53 @@
package kr.co.vividnext.sodalive.mypage.profile.tag
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
class MemberTagViewModel(private val repository: MemberTagRepository) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private val _tagLiveData = MutableLiveData<List<MemberTagResponse>>()
val tagLiveData: LiveData<List<MemberTagResponse>>
get() = _tagLiveData
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
get() = _isLoading
fun getTags() {
_isLoading.value = true
compositeDisposable.add(
repository.getTags("Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_tagLiveData.postValue(it.data!!)
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}

View File

@@ -6,6 +6,9 @@ import kr.co.vividnext.sodalive.explorer.profile.MemberBlockRequest
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser
import kr.co.vividnext.sodalive.main.PushTokenUpdateRequest
import kr.co.vividnext.sodalive.mypage.MyPageResponse
import kr.co.vividnext.sodalive.mypage.profile.ProfileResponse
import kr.co.vividnext.sodalive.mypage.profile.ProfileUpdateRequest
import kr.co.vividnext.sodalive.mypage.profile.nickname.GetChangeNicknamePriceResponse
import kr.co.vividnext.sodalive.settings.notification.GetMemberInfoResponse
import kr.co.vividnext.sodalive.settings.notification.UpdateNotificationSettingRequest
import kr.co.vividnext.sodalive.settings.signout.SignOutRequest
@@ -101,4 +104,37 @@ interface UserApi {
@POST("/member/logout/all")
fun logoutAll(@Header("Authorization") authHeader: String): Single<ApiResponse<Any>>
@GET("/member/change/nickname/price")
fun getChangeNicknamePrice(
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetChangeNicknamePriceResponse>>
@GET("/member/check/nickname")
fun checkNickname(@Query("nickname") nickname: String): Single<ApiResponse<Any>>
@PUT("/member/change/nickname")
fun changeNickname(
@Body request: ProfileUpdateRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@GET("/member")
fun getMyProfile(
@Query("container") container: String = "aos",
@Header("Authorization") authHeader: String
): Single<ApiResponse<ProfileResponse>>
@PUT("/member")
fun updateProfile(
@Body request: ProfileUpdateRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<ProfileResponse>>
@Multipart
@POST("/member/image")
fun updateProfileImage(
@Part multipartFile: MultipartBody.Part,
@Header("Authorization") authHeader: String
): Single<ApiResponse<String>>
}

View File

@@ -6,6 +6,8 @@ import kr.co.vividnext.sodalive.explorer.profile.MemberBlockRequest
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser
import kr.co.vividnext.sodalive.main.PushTokenUpdateRequest
import kr.co.vividnext.sodalive.mypage.MyPageResponse
import kr.co.vividnext.sodalive.mypage.profile.ProfileResponse
import kr.co.vividnext.sodalive.mypage.profile.ProfileUpdateRequest
import kr.co.vividnext.sodalive.settings.notification.UpdateNotificationSettingRequest
import kr.co.vividnext.sodalive.user.find_password.ForgotPasswordRequest
import kr.co.vividnext.sodalive.user.login.LoginRequest
@@ -79,4 +81,31 @@ class UserRepository(private val userApi: UserApi) {
fun logout(token: String) = userApi.logout(authHeader = token)
fun logoutAllDevice(token: String) = userApi.logoutAll(authHeader = token)
fun getChangeNicknamePrice(token: String) = userApi.getChangeNicknamePrice(authHeader = token)
fun checkNickname(nickname: String) = userApi.checkNickname(nickname)
fun changeNickname(
request: ProfileUpdateRequest,
token: String
) = userApi.changeNickname(request = request, authHeader = token)
fun updateProfileImage(
multipartFile: MultipartBody.Part,
token: String
): Single<ApiResponse<String>> {
return userApi.updateProfileImage(multipartFile, authHeader = token)
}
fun updateProfile(
request: ProfileUpdateRequest,
token: String
): Single<ApiResponse<ProfileResponse>> {
return userApi.updateProfile(request, authHeader = token)
}
fun getProfile(token: String): Single<ApiResponse<ProfileResponse>> {
return userApi.getMyProfile(authHeader = token)
}
}