프로필 수정 페이지 추가

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

View File

@ -101,6 +101,9 @@
<activity android:name=".live.reservation.all.LiveReservationAllActivity" />
<activity android:name=".mypage.service_center.ServiceCenterActivity" />
<activity android:name=".onboarding.OnBoardingActivity" />
<activity android:name=".mypage.profile.ProfileUpdateActivity" />
<activity android:name=".mypage.profile.nickname.NicknameUpdateActivity" />
<activity android:name=".mypage.profile.password.ModifyPasswordActivity" />
<activity
android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity"

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)
}
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="30dp" />
<stroke
android:width="1dp"
android:color="@color/color_9970ff" />
</shape>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/color_9970ff" />
<corners android:radius="8dp" />
<stroke
android:width="1dp"
android:color="@color/color_9970ff" />
</shape>

View File

@ -0,0 +1,177 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:orientation="vertical">
<include
android:id="@+id/toolbar"
layout="@layout/detail_toolbar" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/fl_modify_password"
android:layout_below="@+id/toolbar">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="40dp"
android:fontFamily="sans-serif-medium"
android:gravity="center"
android:lineSpacingExtra="6.7sp"
android:text="안전한 비밀번호로 내 내 정보를 보호하세요"
android:textColor="@color/color_eeeeee"
android:textSize="13.3sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="26.7dp"
android:layout_marginTop="40dp"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="6.7dp"
android:fontFamily="@font/gmarket_sans_medium"
android:text="현재 비밀번호"
android:textColor="@color/color_eeeeee"
android:textSize="12sp" />
<EditText
android:id="@+id/et_current_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/edittext_underline"
android:fontFamily="sans-serif-medium"
android:hint="현재 비밀번호를 입력해주세요."
android:importantForAutofill="no"
android:inputType="textWebPassword"
android:paddingHorizontal="6.7dp"
android:paddingTop="12dp"
android:paddingBottom="8dp"
android:textColor="@color/color_eeeeee"
android:textColorHint="@color/color_777777"
android:textCursorDrawable="@drawable/edit_text_cursor"
android:textSize="13.3sp"
android:theme="@style/EditTextStyle"
tools:ignore="LabelFor" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="26.7dp"
android:layout_marginTop="26.7dp"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="6.7dp"
android:fontFamily="@font/gmarket_sans_medium"
android:text="신규 비밀번호"
android:textColor="@color/color_eeeeee"
android:textSize="12sp" />
<EditText
android:id="@+id/et_new_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/edittext_underline"
android:fontFamily="sans-serif-medium"
android:hint="신규 비밀번호를 입력해주세요(영문, 숫자 포함 8자 이상)"
android:importantForAutofill="no"
android:inputType="textWebPassword"
android:paddingHorizontal="6.7dp"
android:paddingTop="12dp"
android:paddingBottom="8dp"
android:textColor="@color/color_eeeeee"
android:textColorHint="@color/color_777777"
android:textCursorDrawable="@drawable/edit_text_cursor"
android:textSize="13.3sp"
android:theme="@style/EditTextStyle"
tools:ignore="LabelFor" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="26.7dp"
android:layout_marginTop="26.7dp"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="6.7dp"
android:fontFamily="@font/gmarket_sans_medium"
android:text="신규 비밀번호 확인"
android:textColor="@color/color_eeeeee"
android:textSize="12sp" />
<EditText
android:id="@+id/et_new_password_confirm"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/edittext_underline"
android:fontFamily="sans-serif-medium"
android:hint="신규 비밀번호를 재입력해주세요"
android:importantForAutofill="no"
android:inputType="textWebPassword"
android:paddingHorizontal="6.7dp"
android:paddingTop="12dp"
android:paddingBottom="8dp"
android:textColor="@color/color_eeeeee"
android:textColorHint="@color/color_777777"
android:textCursorDrawable="@drawable/edit_text_cursor"
android:textSize="13.3sp"
android:theme="@style/EditTextStyle"
tools:ignore="LabelFor" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="26.7dp"
android:layout_marginTop="13.7dp"
android:fontFamily="@font/gmarket_sans_medium"
android:text="* 영문, 숫자 포함 8자 이상"
android:textColor="@color/color_dd4500"
android:textSize="12sp" />
</LinearLayout>
</ScrollView>
<FrameLayout
android:id="@+id/fl_modify_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="@drawable/bg_top_round_corner_16_7_222222">
<TextView
android:id="@+id/tv_modify_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="13.3dp"
android:layout_marginVertical="13.7dp"
android:background="@drawable/bg_round_corner_10_9970ff"
android:fontFamily="@font/gmarket_sans_bold"
android:gravity="center"
android:paddingVertical="16dp"
android:text="비밀번호 변경하기"
android:textColor="@color/white"
android:textSize="18.3sp" />
</FrameLayout>
</RelativeLayout>

View File

@ -0,0 +1,111 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include
android:id="@+id/toolbar"
layout="@layout/detail_toolbar" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/toolbar">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:layout_marginTop="40dp"
android:fontFamily="@font/gmarket_sans_medium"
android:text="닉네임 변경으로 인해 피해를 입는 사용자가 지속적으로 발생하여 닉네임 변경을 부득이하게 유료로 전환합니다."
android:textColor="@color/color_eeeeee" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:fontFamily="@font/gmarket_sans_medium"
android:text="최초 1회에 한해서 무료로 변경이 가능하고, 그 이후부터는 유료로 전환됩니다."
android:textColor="@color/color_dd4500" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:layout_marginTop="40dp"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="6.7dp"
android:fontFamily="@font/gmarket_sans_medium"
android:text="닉네임 (최대 12자)"
android:textColor="@color/color_eeeeee"
android:textSize="12sp" />
<EditText
android:id="@+id/et_nickname"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/edittext_underline"
android:fontFamily="@font/gmarket_sans_medium"
android:hint="닉네임"
android:importantForAutofill="no"
android:inputType="textWebEditText"
android:maxLength="12"
android:paddingHorizontal="6.7dp"
android:textColor="@color/color_eeeeee"
android:textColorHint="@color/color_777777"
android:textCursorDrawable="@drawable/edit_text_cursor"
android:textSize="13.3sp"
android:theme="@style/EditTextStyle"
tools:ignore="LabelFor" />
</LinearLayout>
<TextView
android:id="@+id/tv_check_nickname"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:layout_marginTop="21.3dp"
android:background="@drawable/bg_round_corner_8_transparent_9970ff"
android:fontFamily="@font/gmarket_sans_bold"
android:gravity="center"
android:paddingVertical="13.3dp"
android:text="중복확인"
android:textColor="@color/color_eeeeee" />
</LinearLayout>
</ScrollView>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginTop="26.7dp"
android:background="@drawable/bg_top_round_corner_16_7_222222"
android:paddingHorizontal="13.3dp"
android:paddingVertical="13.7dp">
<TextView
android:id="@+id/tv_change_nickname"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_round_corner_10_9970ff"
android:fontFamily="@font/gmarket_sans_bold"
android:gravity="center"
android:paddingVertical="16dp"
android:textColor="@color/white"
android:textSize="18.3sp"
tools:text="1000코인으로 닉네임 변경하기" />
</FrameLayout>
</RelativeLayout>

View File

@ -0,0 +1,532 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black">
<include
android:id="@+id/toolbar"
layout="@layout/detail_toolbar" />
<ScrollView
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/fl_save"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical">
<RelativeLayout
android:layout_width="85dp"
android:layout_height="85dp"
android:layout_marginTop="13.3dp">
<ImageView
android:id="@+id/iv_profile"
android:layout_width="80dp"
android:layout_height="80dp"
android:adjustViewBounds="true"
android:contentDescription="@null"
tools:src="@drawable/ic_launcher_background" />
<ImageView
android:id="@+id/iv_photo_picker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:background="@drawable/bg_round_corner_33_3_9970ff"
android:contentDescription="@null"
android:padding="10dp"
android:src="@drawable/ic_camera" />
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="13.3dp"
android:layout_marginTop="26.7dp"
android:background="@drawable/bg_round_corner_6_7_222222"
android:orientation="vertical"
android:paddingHorizontal="13.3dp"
android:paddingVertical="20dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="6.7dp"
android:fontFamily="@font/gmarket_sans_medium"
android:text="이메일"
android:textColor="@color/color_eeeeee"
android:textSize="12sp" />
<TextView
android:id="@+id/tv_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="6.7dp"
android:layout_marginTop="6.7dp"
android:fontFamily="@font/gmarket_sans_medium"
android:text="tkdekatk121212@gmail.com"
android:textColor="@color/color_777777"
android:textSize="13.3sp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/color_b3909090" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="26.7dp"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="6.7dp"
android:fontFamily="@font/gmarket_sans_medium"
android:text="비밀번호"
android:textColor="@color/color_eeeeee"
android:textSize="12sp" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginStart="6.7dp"
android:layout_toStartOf="@+id/tv_modify_password"
android:fontFamily="@font/gmarket_sans_medium"
android:gravity="center_vertical"
android:text="********"
android:textColor="@color/color_777777"
android:textSize="13.3sp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_alignBottom="@+id/tv_modify_password"
android:layout_toStartOf="@+id/tv_modify_password"
android:background="@color/color_b3909090" />
<TextView
android:id="@+id/tv_modify_password"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_marginStart="13.3dp"
android:background="@drawable/bg_round_corner_8_9970ff"
android:fontFamily="@font/gmarket_sans_bold"
android:paddingHorizontal="22.7dp"
android:paddingVertical="13.3dp"
android:text="비밀번호 변경"
android:textColor="@color/white"
android:textSize="13.3sp" />
</RelativeLayout>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="13.3dp"
android:layout_marginTop="13.3dp"
android:background="@drawable/bg_round_corner_6_7_222222"
android:orientation="vertical"
android:paddingHorizontal="13.3dp"
android:paddingTop="20dp"
android:paddingBottom="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="6.7dp"
android:fontFamily="@font/gmarket_sans_medium"
android:text="닉네임"
android:textColor="@color/color_eeeeee"
android:textSize="12sp" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp">
<TextView
android:id="@+id/tv_nickname"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginStart="6.7dp"
android:layout_toStartOf="@+id/tv_change_nickname"
android:fontFamily="@font/gmarket_sans_medium"
android:gravity="center_vertical"
android:textColor="@color/color_eeeeee"
android:textSize="13.3sp"
tools:text="닉네임" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_alignBottom="@+id/tv_change_nickname"
android:layout_toStartOf="@+id/tv_change_nickname"
android:background="@color/color_b3909090" />
<TextView
android:id="@+id/tv_change_nickname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_marginStart="13.3dp"
android:background="@drawable/bg_round_corner_8_9970ff"
android:fontFamily="@font/gmarket_sans_bold"
android:paddingHorizontal="22.7dp"
android:paddingVertical="13.3dp"
android:text="닉네임 변경"
android:textColor="@color/white"
android:textSize="13.3sp" />
</RelativeLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16.7dp"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="6.7dp"
android:fontFamily="@font/gmarket_sans_bold"
android:text="성별"
android:textColor="@color/color_eeeeee"
android:textSize="12sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingHorizontal="6.7dp"
android:paddingVertical="13.3dp">
<TextView
android:id="@+id/tv_female"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:button="@null"
android:drawablePadding="13.3dp"
android:fontFamily="@font/gmarket_sans_medium"
android:text="여자"
android:textColor="@color/color_eeeeee"
android:textSize="13.3sp"
app:drawableStartCompat="@drawable/ic_radio_button_select" />
<TextView
android:id="@+id/tv_male"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:button="@null"
android:drawablePadding="13.3dp"
android:fontFamily="@font/gmarket_sans_medium"
android:text="남자"
android:textColor="@color/color_eeeeee"
android:textSize="13.3sp"
app:drawableStartCompat="@drawable/ic_radio_button_select" />
<TextView
android:id="@+id/tv_none"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:button="@null"
android:drawablePadding="13.3dp"
android:fontFamily="@font/gmarket_sans_medium"
android:text="공개 안 함"
android:textColor="@color/color_eeeeee"
android:textSize="13.3sp"
app:drawableStartCompat="@drawable/ic_radio_button_select" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="13.3dp"
android:layout_marginTop="26.7dp"
android:layout_marginBottom="13.3dp"
android:background="@drawable/bg_round_corner_6_7_222222"
android:orientation="vertical"
android:paddingHorizontal="13.3dp"
android:paddingVertical="20dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="6.7dp"
android:fontFamily="@font/gmarket_sans_medium"
android:text="인스타그램"
android:textColor="@color/color_eeeeee"
android:textSize="12sp" />
<EditText
android:id="@+id/et_instagram"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/edittext_underline"
android:fontFamily="@font/gmarket_sans_medium"
android:hint="인스타그램 URL"
android:importantForAutofill="no"
android:inputType="textWebEditText"
android:paddingHorizontal="6.7dp"
android:textColor="@color/color_eeeeee"
android:textColorHint="@color/color_777777"
android:textCursorDrawable="@drawable/edit_text_cursor"
android:textSize="13.3sp"
android:theme="@style/EditTextStyle"
tools:ignore="LabelFor" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="18dp"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="6.7dp"
android:fontFamily="@font/gmarket_sans_medium"
android:text="유튜브 채널"
android:textColor="@color/color_eeeeee"
android:textSize="12sp" />
<EditText
android:id="@+id/et_youtube"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/edittext_underline"
android:fontFamily="@font/gmarket_sans_medium"
android:hint="유튜브 채널 URL"
android:importantForAutofill="no"
android:inputType="textWebEditText"
android:paddingHorizontal="6.7dp"
android:textColor="@color/color_eeeeee"
android:textColorHint="@color/color_777777"
android:textCursorDrawable="@drawable/edit_text_cursor"
android:textSize="13.3sp"
android:theme="@style/EditTextStyle"
tools:ignore="LabelFor" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="18dp"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="6.7dp"
android:fontFamily="@font/gmarket_sans_medium"
android:text="웹사이트"
android:textColor="@color/color_eeeeee"
android:textSize="12sp" />
<EditText
android:id="@+id/et_website"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/edittext_underline"
android:fontFamily="@font/gmarket_sans_medium"
android:hint="웹사이트 URL"
android:importantForAutofill="no"
android:inputType="textWebEditText"
android:paddingHorizontal="6.7dp"
android:textColor="@color/color_eeeeee"
android:textColorHint="@color/color_777777"
android:textCursorDrawable="@drawable/edit_text_cursor"
android:textSize="13.3sp"
android:theme="@style/EditTextStyle"
tools:ignore="LabelFor" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="18dp"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="6.7dp"
android:fontFamily="@font/gmarket_sans_medium"
android:text="블로그"
android:textColor="@color/color_eeeeee"
android:textSize="12sp" />
<EditText
android:id="@+id/et_blog"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/edittext_underline"
android:fontFamily="@font/gmarket_sans_medium"
android:hint="블로그 URL"
android:importantForAutofill="no"
android:inputType="textWebEditText"
android:paddingHorizontal="6.7dp"
android:textColor="@color/color_eeeeee"
android:textColorHint="@color/color_777777"
android:textCursorDrawable="@drawable/edit_text_cursor"
android:textSize="13.3sp"
android:theme="@style/EditTextStyle"
tools:ignore="LabelFor" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="13.3dp"
android:layout_marginTop="26.7dp"
android:background="@drawable/bg_round_corner_6_7_222222"
android:orientation="vertical"
android:paddingHorizontal="13.3dp"
android:paddingVertical="20dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/gmarket_sans_bold"
android:text="관심사"
android:textColor="@color/color_eeeeee"
android:textSize="16.7sp" />
<TextView
android:id="@+id/tv_select_tag"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="13.3dp"
android:background="@drawable/bg_round_corner_24_3_339970ff_9970ff"
android:fontFamily="@font/gmarket_sans_bold"
android:gravity="center"
android:paddingVertical="13.7dp"
android:text="관심사 선택"
android:textColor="@color/color_9970ff"
android:textSize="16.7sp" />
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="none">
<LinearLayout
android:id="@+id/ll_select_tags"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="13.3dp"
android:orientation="horizontal"
android:visibility="gone" />
</HorizontalScrollView>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="13.3dp"
android:layout_marginVertical="40dp"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/gmarket_sans_bold"
android:lineSpacingExtra="5sp"
android:text="소개글"
android:textColor="@color/color_eeeeee"
android:textSize="16.7sp" />
<EditText
android:id="@+id/et_introduce"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="13.3dp"
android:background="@drawable/bg_round_corner_6_7_222222"
android:fontFamily="@font/gmarket_sans_medium"
android:gravity="top"
android:hint="소개글을 입력하세요"
android:importantForAutofill="no"
android:inputType="textMultiLine"
android:minHeight="200dp"
android:padding="20dp"
android:textColor="@color/color_bbbbbb"
android:textColorHint="@color/color_777777"
android:textCursorDrawable="@drawable/edit_text_cursor"
android:textSize="13.3sp"
tools:ignore="LabelFor" />
</LinearLayout>
</LinearLayout>
</ScrollView>
<FrameLayout
android:id="@+id/fl_save"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@drawable/bg_top_round_corner_16_7_222222"
android:paddingHorizontal="13.3dp"
android:paddingVertical="13.7dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<TextView
android:id="@+id/tv_save"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_round_corner_10_9970ff"
android:fontFamily="@font/gmarket_sans_bold"
android:gravity="center"
android:paddingVertical="16dp"
android:text="저장하기"
android:textColor="@color/white"
android:textSize="18.3sp" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/color_222222"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="26.7dp"
android:layout_marginTop="26.7dp"
android:fontFamily="@font/gmarket_sans_bold"
android:text="관심사 선택"
android:textColor="@color/white"
android:textSize="18.3sp" />
<ImageView
android:id="@+id/iv_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:contentDescription="@null"
android:padding="29dp"
android:src="@drawable/ic_close_white" />
</RelativeLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_tags"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp" />
<TextView
android:id="@+id/tv_select"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="13.3dp"
android:background="@drawable/bg_round_corner_10_9970ff"
android:fontFamily="@font/gmarket_sans_bold"
android:gravity="center"
android:paddingVertical="16dp"
android:text="선택하기"
android:textColor="@color/white"
android:textSize="18.3sp" />
</LinearLayout>