diff --git a/app/src/main/java/com/eatssu/android/data/repository/UserRepositoryImpl.kt b/app/src/main/java/com/eatssu/android/data/repository/UserRepositoryImpl.kt index 59b8370a0..12c41ba66 100644 --- a/app/src/main/java/com/eatssu/android/data/repository/UserRepositoryImpl.kt +++ b/app/src/main/java/com/eatssu/android/data/repository/UserRepositoryImpl.kt @@ -3,7 +3,6 @@ package com.eatssu.android.data.repository import com.eatssu.android.data.dto.request.ChangeNicknameRequest import com.eatssu.android.data.dto.request.UserDepartmentRequest import com.eatssu.android.data.dto.response.BaseResponse -import com.eatssu.android.data.dto.response.MyNickNameResponse import com.eatssu.android.data.dto.response.MyReviewResponse import com.eatssu.android.data.dto.response.toDomain import com.eatssu.android.data.service.UserService @@ -17,10 +16,10 @@ import javax.inject.Inject class UserRepositoryImpl @Inject constructor(private val userService: UserService) : UserRepository { - override suspend fun updateUserName(body: ChangeNicknameRequest): Flow> = - flow { - emit(userService.changeNickname(body)) - } + override suspend fun updateUserName(body: ChangeNicknameRequest) { + userService.changeNickname(body) + } + override suspend fun checkUserNameValidation(nickname: String): Flow> = @@ -33,10 +32,7 @@ class UserRepositoryImpl @Inject constructor(private val userService: UserServic emit(userService.getMyReviews()) } - override suspend fun getUserNickName(): Flow> = - flow { - emit(userService.getMyInfo()) - } + override suspend fun getUserNickName() = userService.getMyInfo().result?.nickname ?: "" override suspend fun signOut(): Boolean { return userService.signOut().result ?: false diff --git a/app/src/main/java/com/eatssu/android/domain/repository/UserRepository.kt b/app/src/main/java/com/eatssu/android/domain/repository/UserRepository.kt index 17adc9649..fcfac9800 100644 --- a/app/src/main/java/com/eatssu/android/domain/repository/UserRepository.kt +++ b/app/src/main/java/com/eatssu/android/domain/repository/UserRepository.kt @@ -2,7 +2,6 @@ package com.eatssu.android.domain.repository import com.eatssu.android.data.dto.request.ChangeNicknameRequest import com.eatssu.android.data.dto.response.BaseResponse -import com.eatssu.android.data.dto.response.MyNickNameResponse import com.eatssu.android.data.dto.response.MyReviewResponse import com.eatssu.android.domain.model.College import com.eatssu.android.domain.model.Department @@ -12,14 +11,14 @@ interface UserRepository { suspend fun updateUserName( body: ChangeNicknameRequest, - ): Flow> + ) suspend fun checkUserNameValidation( nickname: String, ): Flow> suspend fun getUserReviews(): Flow> - suspend fun getUserNickName(): Flow> + suspend fun getUserNickName(): String suspend fun signOut(): Boolean // 모든 단과대 조회 diff --git a/app/src/main/java/com/eatssu/android/domain/usecase/user/GetUserNickNameUseCase.kt b/app/src/main/java/com/eatssu/android/domain/usecase/user/GetUserNickNameUseCase.kt index bef950056..536cc45d3 100644 --- a/app/src/main/java/com/eatssu/android/domain/usecase/user/GetUserNickNameUseCase.kt +++ b/app/src/main/java/com/eatssu/android/domain/usecase/user/GetUserNickNameUseCase.kt @@ -2,21 +2,18 @@ package com.eatssu.android.domain.usecase.user import android.content.Context import com.eatssu.android.data.MySharedPreferences -import com.eatssu.android.data.dto.response.BaseResponse -import com.eatssu.android.data.dto.response.MyNickNameResponse import com.eatssu.android.domain.repository.UserRepository -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.onEach import javax.inject.Inject class GetUserNickNameUseCase @Inject constructor( private val userRepository: UserRepository, private val context: Context // SharedPreferences 접근용 ) { - suspend operator fun invoke(): Flow> = - userRepository.getUserNickName().onEach { response -> - response.result?.let { nicknameResponse -> - MySharedPreferences.setUserName(context, nicknameResponse.nickname ?: "") - } + suspend operator fun invoke(): String { + return MySharedPreferences.getUserName(context).ifEmpty { + val remoteNickname = userRepository.getUserNickName() + MySharedPreferences.setUserName(context, remoteNickname) + remoteNickname } + } } diff --git a/app/src/main/java/com/eatssu/android/domain/usecase/user/SetUserNicknameUseCase.kt b/app/src/main/java/com/eatssu/android/domain/usecase/user/SetUserNicknameUseCase.kt index 0e6c49da2..7b2302c8e 100644 --- a/app/src/main/java/com/eatssu/android/domain/usecase/user/SetUserNicknameUseCase.kt +++ b/app/src/main/java/com/eatssu/android/domain/usecase/user/SetUserNicknameUseCase.kt @@ -3,11 +3,8 @@ package com.eatssu.android.domain.usecase.user import android.content.Context import com.eatssu.android.data.MySharedPreferences import com.eatssu.android.data.dto.request.ChangeNicknameRequest -import com.eatssu.android.data.dto.response.BaseResponse -import com.eatssu.android.domain.model.UserInfo import com.eatssu.android.domain.repository.UserRepository import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.flow.Flow import javax.inject.Inject //class SetUserNameUseCase @Inject constructor( @@ -26,10 +23,9 @@ class SetUserNicknameUseCase @Inject constructor( private val userRepository: UserRepository, @ApplicationContext private val context: Context ) { - suspend operator fun invoke(nickname: String): Flow> { + suspend operator fun invoke(nickname: String) { // 로컬 저장 MySharedPreferences.setUserName(context, nickname) - - return userRepository.updateUserName(ChangeNicknameRequest(nickname)) + userRepository.updateUserName(ChangeNicknameRequest(nickname)) } } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/presentation/MainViewModel.kt b/app/src/main/java/com/eatssu/android/presentation/MainViewModel.kt index 05076ceaa..fd73c8981 100644 --- a/app/src/main/java/com/eatssu/android/presentation/MainViewModel.kt +++ b/app/src/main/java/com/eatssu/android/presentation/MainViewModel.kt @@ -13,15 +13,14 @@ import com.eatssu.android.domain.usecase.user.GetUserNickNameUseCase import com.eatssu.android.domain.usecase.user.SetUserCollegeDepartmentUseCase import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import timber.log.Timber import java.time.LocalDate import javax.inject.Inject @@ -56,35 +55,37 @@ class MainViewModel @Inject constructor( ) } - fun fetchAndCheckNickname() { + private fun fetchAndCheckNickname() { viewModelScope.launch { - getUserNickNameUseCase().onStart { - _uiState.value = UiState.Loading - }.catch { e -> - _uiState.value = UiState.Error - _uiEvent.emit(UiEvent.ShowToast(context.getString(R.string.not_found))) - Timber.e(e.toString()) - }.collectLatest { result -> - Timber.d(result.toString()) - result.result?.let { userInfo -> - val nickname = userInfo.nickname - - if (nickname.isNullOrBlank() || nickname.startsWith("user-")) { - _uiState.value = UiState.Success(MainState.NicknameNull) - _uiEvent.emit(UiEvent.ShowToast(context.getString(R.string.set_nickname))) - return@let - } - - _uiState.value = UiState.Success(MainState.NicknameExists(nickname)) - _uiEvent.emit( - UiEvent.ShowToast( - String.format( - context.getString(R.string.hello_user), - nickname - ) + _uiState.value = UiState.Loading + runCatching { + withContext(Dispatchers.IO) { getUserNickNameUseCase() } + }.onSuccess { nickname -> + // 1) 닉네임 없음/기본 프리셋 + if (nickname.isNullOrBlank() || nickname.startsWith("user-")) { + _uiState.value = UiState.Success(MainState.NicknameNull) + _uiEvent.emit(UiEvent.ShowToast(context.getString(R.string.set_nickname))) + return@launch // ← 아래 분기 실행 막기 + } + + // 2) 정상 닉네임 + _uiState.value = UiState.Success(MainState.NicknameExists(nickname)) + _uiEvent.emit( + UiEvent.ShowToast( + String.format( + context.getString(R.string.hello_user), + nickname ) ) - } + ) + }.onFailure { e -> + _uiState.value = UiState.Error + _uiEvent.emit( + UiEvent.ShowToast( + context.getString(R.string.not_found) + ) + ) + Timber.e(e) } } } diff --git a/app/src/main/java/com/eatssu/android/presentation/mypage/MyPageFragment.kt b/app/src/main/java/com/eatssu/android/presentation/mypage/MyPageFragment.kt index 44fb9f2ae..e9dec35e6 100644 --- a/app/src/main/java/com/eatssu/android/presentation/mypage/MyPageFragment.kt +++ b/app/src/main/java/com/eatssu/android/presentation/mypage/MyPageFragment.kt @@ -4,7 +4,6 @@ import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.graphics.Paint -import android.net.Uri import android.os.Build import android.os.Bundle import android.provider.Settings @@ -12,6 +11,7 @@ import android.view.LayoutInflater import android.view.View import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat +import androidx.core.net.toUri import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle @@ -20,15 +20,18 @@ import androidx.lifecycle.repeatOnLifecycle import com.eatssu.android.R import com.eatssu.android.databinding.FragmentMyPageBinding import com.eatssu.android.presentation.MainViewModel +import com.eatssu.android.presentation.UiEvent +import com.eatssu.android.presentation.UiState import com.eatssu.android.presentation.base.BaseFragment import com.eatssu.android.presentation.login.LoginActivity import com.eatssu.android.presentation.mypage.myreview.MyReviewListActivity import com.eatssu.android.presentation.mypage.terms.WebViewActivity import com.eatssu.android.presentation.mypage.userinfo.UserInfoActivity +import com.eatssu.android.presentation.util.showToast import com.eatssu.common.enums.ScreenId import com.google.android.gms.oss.licenses.OssLicensesMenuActivity -import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import timber.log.Timber import java.time.LocalDateTime @@ -46,32 +49,65 @@ class MyPageFragment : BaseFragment(ScreenId.MYPAGE_MAIN) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - binding.tvSignout.paintFlags = Paint.UNDERLINE_TEXT_FLAG setupObservers() setOnClickListener() } + override fun onResume() { + super.onResume() + myPageViewModel.fetchMyInfo() // 닉네임 변경 등으로부터 복귀 시 정보 갱신 + } + private fun setupObservers() { viewLifecycleOwner.lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { - myPageViewModel.uiState.collect { - binding.tvAppVersion.text = it.appVersion + launch { + myPageViewModel.uiState.collectLatest { ui -> + when (ui) { + is UiState.Init, UiState.Loading -> Unit // 닉네임만 불러옴으로 로딩 인디케이터 없음 + is UiState.Success -> { + ui.data?.let { render(it) } + } - if (it.nickname.isNotEmpty()) { - binding.tvNickname.text = it.nickname + is UiState.Error -> { + showToast(getString(R.string.not_found)) + } + } } - - binding.alarmSwitch.setOnCheckedChangeListener(null) - binding.alarmSwitch.isChecked = it.isAlarmOn - binding.alarmSwitch.setOnCheckedChangeListener { _, isChecked -> - handleAlarmSwitchChange(isChecked) + } + launch { + myPageViewModel.uiEvent.collectLatest { event -> + when (event) { + is UiEvent.ShowToast -> showToast(event.message) + else -> Unit + } } } } } } + private fun render(state: MyPageState) { + // 앱 버전 + binding.tvAppVersion.text = state.appVersion + + // 닉네임 + if (state.hasNickname) { + binding.tvNickname.text = state.nickname + } else { + // 필요 시 미설정 안내 문구 + binding.tvNickname.text = "닉네임을 설정해주세요" + } + + // 알람 스위치 (리스너 잠시 해제 후 값 반영) + binding.alarmSwitch.setOnCheckedChangeListener(null) + binding.alarmSwitch.isChecked = state.isAlarmOn + binding.alarmSwitch.setOnCheckedChangeListener { _, isChecked -> + handleAlarmSwitchChange(isChecked) + } + } + private fun handleAlarmSwitchChange(isChecked: Boolean) { val nowDatetime = LocalDateTime.now() val formattedDate = nowDatetime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) @@ -79,13 +115,19 @@ class MyPageFragment : BaseFragment(ScreenId.MYPAGE_MAIN) if (isChecked) { if (checkNotificationPermission(requireContext())) { myPageViewModel.setNotificationOn() - showSnackbar("EAT-SSU 알림 수신을 동의하였습니다.\n$formattedDate") + showToast("EAT-SSU 알림 수신을 동의하였습니다.\n$formattedDate") } else { showNotificationPermissionDialog() + // 권한 미허용이면 스위치 원복 + binding.alarmSwitch.setOnCheckedChangeListener(null) + binding.alarmSwitch.isChecked = false + binding.alarmSwitch.setOnCheckedChangeListener { _, checked -> + handleAlarmSwitchChange(checked) + } } } else { myPageViewModel.setNotificationOff() - showSnackbar("EAT-SSU 알림 수신을 거부하였습니다.\n$formattedDate") + showToast("EAT-SSU 알림 수신을 거부하였습니다.\n$formattedDate") } } @@ -111,8 +153,10 @@ class MyPageFragment : BaseFragment(ScreenId.MYPAGE_MAIN) } binding.llSignout.setOnClickListener { + // 현재 Success 상태에서 안전하게 닉네임 추출 + val nickname = (myPageViewModel.uiState.value as? UiState.Success)?.data?.nickname Intent(requireContext(), SignOutActivity::class.java).apply { - putExtra("nickname", myPageViewModel.uiState.value.nickname) + putExtra("nickname", nickname) startActivity(this) } } @@ -121,13 +165,9 @@ class MyPageFragment : BaseFragment(ScreenId.MYPAGE_MAIN) startActivity(Intent(requireContext(), DeveloperActivity::class.java)) } - binding.llOss.setOnClickListener { - moveToOss() - } + binding.llOss.setOnClickListener { moveToOss() } - binding.llAppVersion.setOnClickListener { - moveToPlayStore() - } + binding.llAppVersion.setOnClickListener { moveToPlayStore() } binding.llServiceRule.setOnClickListener { startWebView( @@ -182,16 +222,15 @@ class MyPageFragment : BaseFragment(ScreenId.MYPAGE_MAIN) try { startActivity(Intent(requireContext(), OssLicensesMenuActivity::class.java)) } catch (e: Exception) { - showSnackbar("오픈소스 라이브러리를 불러올 수 없습니다.") + showToast("오픈소스 라이브러리를 불러올 수 없습니다.") Timber.e("Error opening OSS Licenses: ${e.message}") } } private fun moveToPlayStore() { val appPackageName = requireContext().packageName - val uri = Uri.parse("market://details?id=$appPackageName") - val fallbackUri = Uri.parse("https://play.google.com/store/apps/details?id=$appPackageName") - + val uri = "market://details?id=$appPackageName".toUri() + val fallbackUri = "https://play.google.com/store/apps/details?id=$appPackageName".toUri() try { startActivity(Intent(Intent.ACTION_VIEW, uri)) } catch (e: Exception) { @@ -206,10 +245,6 @@ class MyPageFragment : BaseFragment(ScreenId.MYPAGE_MAIN) context.startActivity(intent) } - private fun showSnackbar(message: String) { - Snackbar.make(binding.root, message, Snackbar.LENGTH_SHORT).show() - } - private fun startWebView(url: String, title: String, screenId: ScreenId) { val intent = Intent(requireContext(), WebViewActivity::class.java).apply { putExtra("URL", url) diff --git a/app/src/main/java/com/eatssu/android/presentation/mypage/MyPageViewModel.kt b/app/src/main/java/com/eatssu/android/presentation/mypage/MyPageViewModel.kt index 8e8af2a69..a2264abfb 100644 --- a/app/src/main/java/com/eatssu/android/presentation/mypage/MyPageViewModel.kt +++ b/app/src/main/java/com/eatssu/android/presentation/mypage/MyPageViewModel.kt @@ -7,16 +7,21 @@ import com.eatssu.android.data.repository.PreferencesRepository import com.eatssu.android.domain.usecase.alarm.AlarmUseCase import com.eatssu.android.domain.usecase.alarm.SetDailyNotificationStatusUseCase import com.eatssu.android.domain.usecase.user.GetUserNickNameUseCase +import com.eatssu.android.presentation.UiEvent +import com.eatssu.android.presentation.UiState import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.onCompletion -import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import timber.log.Timber import javax.inject.Inject @@ -25,75 +30,63 @@ class MyPageViewModel @Inject constructor( private val getUserNickNameUseCase: GetUserNickNameUseCase, private val setNotificationStatusUseCase: SetDailyNotificationStatusUseCase, private val alarmUseCase: AlarmUseCase, - private val preferencesRepository: PreferencesRepository // Assuming you're using DataStore here + private val preferencesRepository: PreferencesRepository ) : ViewModel() { - private val _uiState: MutableStateFlow = MutableStateFlow(MyPageState()) - val uiState: StateFlow = _uiState.asStateFlow() + // 내부는 항상 "값 그 자체"만 들고 있고, + // 화면엔 UiState로 감싸서 노출 + private val _state = MutableStateFlow( + MyPageState(appVersion = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})") + ) + val uiState: StateFlow> = + _state + .map { UiState.Success(it) } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), UiState.Init) - init { - setAppVersion() - getMyInfo() - getNotificationStatus() - } + // 이벤트 버퍼를 주면 토스트 연속 발생 시 유실을 줄일 수 있음 + private val _uiEvent = MutableSharedFlow( + replay = 0, + extraBufferCapacity = 1 + ) + val uiEvent: SharedFlow = _uiEvent - private fun setAppVersion() { - viewModelScope.launch { - _uiState.value = - _uiState.value.copy(appVersion = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})") - } + init { + observeNotificationStatus() + fetchMyInfo() } - private fun getNotificationStatus() { + private fun observeNotificationStatus() { viewModelScope.launch { - preferencesRepository.dailyNotificationStatus.collect { isAlarmOn -> - _uiState.value = _uiState.value.copy(isAlarmOn = isAlarmOn) + preferencesRepository.dailyNotificationStatus.collectLatest { isOn -> + _state.update { it.copy(isAlarmOn = isOn) } } } } - private fun getMyInfo() { + fun fetchMyInfo() { viewModelScope.launch { - getUserNickNameUseCase().onStart { - _uiState.update { it.copy(loading = true) } - }.onCompletion { - _uiState.update { it.copy(loading = false, error = true) } - }.catch { e -> - _uiState.update { it.copy(error = true, toastMessage = "정보를 불러올 수 없습니다.") } - Timber.e(e.toString()) - }.collectLatest { result -> - Timber.d(result.toString()) - result.result?.apply { - if (this.nickname.isNullOrBlank()) { - _uiState.update { - it.copy( - loading = false, - error = false, - isNicknameNull = true, - toastMessage = "닉네임을 설정해주세요." - ) - } - } else { - _uiState.update { - it.copy( - loading = false, - error = false, - isNicknameNull = false, - toastMessage = "${this.nickname} 님 환영합니다.", - nickname = this.nickname.toString(), - platform = this.provider - ) - } - } + runCatching { + withContext(Dispatchers.IO) { getUserNickNameUseCase() } + }.onSuccess { nickname -> + if (nickname.isNullOrBlank() || nickname.startsWith("user-")) { + _state.update { it.copy(nickname = null) } + _uiEvent.emit(UiEvent.ShowToast("닉네임을 설정해주세요.")) + } else { + _state.update { it.copy(nickname = nickname) } } + }.onFailure { e -> + // 에러 화면을 꼭 별도로 보여주고 싶다면 uiState를 에러로 전환하는 방식 선택 + // 여기서는 '상태 유지 + 토스트'만 처리 + _uiEvent.emit(UiEvent.ShowToast("정보를 불러올 수 없습니다.")) + Timber.e(e) } } } fun setNotificationOn() { viewModelScope.launch { - setNotificationStatusUseCase(true) //로컬 디비 저장 - alarmUseCase.scheduleAlarm() //알람 매니저 + setNotificationStatusUseCase(true) + alarmUseCase.scheduleAlarm() } } @@ -105,17 +98,11 @@ class MyPageViewModel @Inject constructor( } } - data class MyPageState( - var loading: Boolean = true, - var error: Boolean = false, - - var toastMessage: String = "", - - var nickname: String = "", - var platform: String = "", - var isAlarmOn: Boolean = false, - var appVersion: String = "0.0.0", - - var isNicknameNull: Boolean = false, -) \ No newline at end of file + val nickname: String? = null, + val platform: String = "KAKAO", + val isAlarmOn: Boolean = false, + val appVersion: String = "0.0.0" +) { + val hasNickname: Boolean get() = !nickname.isNullOrBlank() && !nickname.startsWith("user-") +} diff --git a/app/src/main/java/com/eatssu/android/presentation/mypage/userinfo/UserInfoViewModel.kt b/app/src/main/java/com/eatssu/android/presentation/mypage/userinfo/UserInfoViewModel.kt index 89834d930..f45d95988 100644 --- a/app/src/main/java/com/eatssu/android/presentation/mypage/userinfo/UserInfoViewModel.kt +++ b/app/src/main/java/com/eatssu/android/presentation/mypage/userinfo/UserInfoViewModel.kt @@ -94,16 +94,26 @@ class UserInfoViewModel @Inject constructor( val nickname = _uiState.value.nickname viewModelScope.launch { - setUserNicknameUseCase(nickname) - .onStart { _uiState.update { it.copy(loading = true, error = false) } } - .onCompletion { _uiState.update { it.copy(loading = false, error = false) } } - .catch { - _uiState.update { it.copy(error = true, toastMessage = "정보 저장에 실패했습니다.") } - Timber.e(it) + _uiState.update { it.copy(loading = true) } + try { + setUserNicknameUseCase(nickname) + _uiState.update { + it.copy( + loading = false, + isDone = true, + toastMessage = "닉네임 변경에 성공했습니다." + ) } - .collectLatest { - _uiState.update { it.copy(isDone = true, toastMessage = "정보가 성공적으로 저장되었습니다.") } + } catch (e: Exception) { + Timber.e(e, "닉네임 변경 실패") + _uiState.update { + it.copy( + loading = false, + error = true, + toastMessage = "닉네임 변경에 실패했습니다." + ) } + } } }