diff --git a/app/src/main/java/com/eatssu/android/presentation/MainActivity.kt b/app/src/main/java/com/eatssu/android/presentation/MainActivity.kt index 758d5cf7a..a007b5a29 100644 --- a/app/src/main/java/com/eatssu/android/presentation/MainActivity.kt +++ b/app/src/main/java/com/eatssu/android/presentation/MainActivity.kt @@ -20,6 +20,7 @@ import com.eatssu.android.presentation.base.BaseActivity import com.eatssu.android.presentation.login.LoginActivity import com.eatssu.android.presentation.mypage.MyPageViewModel import com.eatssu.android.presentation.mypage.userinfo.UserInfoActivity +import com.eatssu.android.presentation.util.showInfoToast import com.eatssu.android.presentation.util.showToast import com.eatssu.android.presentation.util.startActivity import com.eatssu.common.UiEvent @@ -114,11 +115,11 @@ class MainActivity : BaseActivity( if (requestCode == 1000) { if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // 권한이 승인됨 - showToast("EAT-SSU 알림 수신을 동의하였습니다.") + showInfoToast("EAT-SSU 알림 수신을 동의하였습니다.") myPageViewModel.setNotificationOn() //바로 알림 받도록 설정 } else { // 권한이 거부됨 - showToast("EAT-SSU 알림 수신을 거부하였습니다.\n$dateFormat") + showInfoToast("EAT-SSU 알림 수신을 거부하였습니다.\n$dateFormat") myPageViewModel.setNotificationOff() //바로 알림 받도록 설정 } } @@ -171,8 +172,8 @@ class MainActivity : BaseActivity( private fun collectUiEvents() { lifecycleScope.launch { mainViewModel.uiEvent.collectLatest { event -> - if (event is UiEvent.ShowToast) { - showToast(event.message) + when (event) { + is UiEvent.ShowToast -> showToast(event) } } } 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 8e00f5519..6a22eab00 100644 --- a/app/src/main/java/com/eatssu/android/presentation/MainViewModel.kt +++ b/app/src/main/java/com/eatssu/android/presentation/MainViewModel.kt @@ -13,6 +13,7 @@ import com.eatssu.android.domain.usecase.user.GetUserNickNameUseCase import com.eatssu.android.domain.usecase.user.SetUserCollegeDepartmentUseCase import com.eatssu.common.UiEvent import com.eatssu.common.UiState +import com.eatssu.common.enums.ToastType import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableSharedFlow @@ -67,20 +68,12 @@ class MainViewModel @Inject constructor( // 1) 닉네임 없음 if (nickname.isBlank()) { _uiState.value = UiState.Success(MainState.NicknameNull) - _uiEvent.emit(UiEvent.ShowToast(context.getString(R.string.set_nickname))) + _uiEvent.emit(UiEvent.ShowToast(context.getString(R.string.set_nickname), ToastType.ERROR)) return@launch } // 2) 정상 닉네임 _uiState.value = UiState.Success(MainState.NicknameExists(nickname)) - _uiEvent.emit( - UiEvent.ShowToast( - String.format( - context.getString(R.string.hello_user), - nickname - ) - ) - ) } } @@ -88,7 +81,11 @@ class MainViewModel @Inject constructor( viewModelScope.launch { logoutUseCase() _uiState.value = UiState.Success(MainState.LoggedOut) - _uiEvent.emit(UiEvent.ShowToast("로그아웃 되었습니다.")) + _uiEvent.emit( + UiEvent.ShowToast( + context.getString(R.string.toast_logout_success), ToastType.SUCCESS + ) + ) } } @@ -121,7 +118,12 @@ class MainViewModel @Inject constructor( viewModelScope.launch { val (college, department) = userRepository.getUserCollegeDepartment() ?: run { _uiState.value = UiState.Error - _uiEvent.emit(UiEvent.ShowToast("정보를 불러올 수 없습니다.")) + _uiEvent.emit( + UiEvent.ShowToast( + context.getString(R.string.not_found), + ToastType.ERROR + ) + ) return@launch } diff --git a/app/src/main/java/com/eatssu/android/presentation/base/BaseActivity.kt b/app/src/main/java/com/eatssu/android/presentation/base/BaseActivity.kt index df746f5a5..d92b08d63 100644 --- a/app/src/main/java/com/eatssu/android/presentation/base/BaseActivity.kt +++ b/app/src/main/java/com/eatssu/android/presentation/base/BaseActivity.kt @@ -9,7 +9,6 @@ import android.view.View import android.view.ViewGroup import android.view.inputmethod.InputMethodManager import android.widget.TextView -import android.widget.Toast import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar @@ -21,6 +20,7 @@ import com.eatssu.android.R import com.eatssu.android.presentation.common.NetworkConnection import com.eatssu.android.presentation.login.LoginActivity import com.eatssu.android.presentation.util.observeNetworkError +import com.eatssu.android.presentation.util.showInfoToast import com.eatssu.common.EventLogger import com.eatssu.common.enums.ScreenId import com.google.android.material.card.MaterialCardView @@ -107,17 +107,14 @@ abstract class BaseActivity( private fun observeTokenExpiration() { lifecycleScope.launch { TokenEventBus.tokenExpired.collect { - Toast.makeText(this@BaseActivity, - getString(R.string.token_expired), Toast.LENGTH_SHORT).show() + showInfoToast(R.string.toast_token_expired) navigateToLogin() } } lifecycleScope.launch { TokenEventBus.tokenServerError.collect { - Toast.makeText(this@BaseActivity, - getString(R.string.token_server_error), Toast.LENGTH_SHORT) - .show() + showInfoToast(R.string.toast_token_server_error) navigateToLogin() } } diff --git a/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/list/ReviewListScreen.kt b/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/list/ReviewListScreen.kt index ed7224819..03ec6ed26 100644 --- a/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/list/ReviewListScreen.kt +++ b/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/list/ReviewListScreen.kt @@ -80,7 +80,7 @@ fun ReviewListScreen( when (uiEvent) { is UiEvent.ShowToast -> { - context.showToast((uiEvent as UiEvent.ShowToast).message) + context.showToast(uiEvent as UiEvent.ShowToast) } } diff --git a/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/list/ReviewListViewModel.kt b/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/list/ReviewListViewModel.kt index a222b1adc..3a880c762 100644 --- a/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/list/ReviewListViewModel.kt +++ b/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/list/ReviewListViewModel.kt @@ -10,6 +10,7 @@ import com.eatssu.android.domain.usecase.review.GetReviewListUseCase import com.eatssu.common.UiEvent import com.eatssu.common.UiState import com.eatssu.common.enums.MenuType +import com.eatssu.common.enums.ToastType import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -53,7 +54,7 @@ class ReviewListViewModel @Inject constructor( _uiState.value = UiState.Success(ReviewListState(reviewInfo, reviewList)) } catch (e: Exception) { _uiState.value = UiState.Error - _uiEvent.emit(UiEvent.ShowToast("리뷰를 불러오지 못했습니다.")) + _uiEvent.emit(UiEvent.ShowToast("리뷰를 불러오지 못했습니다.", ToastType.ERROR)) } } @@ -63,12 +64,12 @@ class ReviewListViewModel @Inject constructor( val success = deleteReviewUseCase(reviewId) if (!success) { - _uiEvent.emit(UiEvent.ShowToast("리뷰 삭제에 실패했습니다.")) + _uiEvent.emit(UiEvent.ShowToast("리뷰 삭제에 실패했습니다.", ToastType.ERROR)) return@launch } // 삭제 성공 시 - _uiEvent.emit(UiEvent.ShowToast("리뷰를 삭제했습니다.")) + _uiEvent.emit(UiEvent.ShowToast("리뷰를 삭제했습니다.", ToastType.SUCCESS)) val type = lastMenuType val id = lastItemId if (type != null && id != null) { diff --git a/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/modify/ModifyReviewScreen.kt b/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/modify/ModifyReviewScreen.kt index 637fb8b98..0818002a0 100644 --- a/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/modify/ModifyReviewScreen.kt +++ b/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/modify/ModifyReviewScreen.kt @@ -65,7 +65,7 @@ fun ModifyReviewScreen( when (event) { is UiEvent.NavigateBack -> onBack() is UiEvent.ShowToast -> { - context.showToast(event.message) + context.showToast(event) } } } diff --git a/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/modify/ModifyViewModel.kt b/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/modify/ModifyViewModel.kt index f408f122c..fcb7a2722 100644 --- a/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/modify/ModifyViewModel.kt +++ b/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/modify/ModifyViewModel.kt @@ -6,6 +6,7 @@ import com.eatssu.android.domain.model.Review import com.eatssu.android.domain.usecase.review.ModifyReviewUseCase import com.eatssu.common.UiEvent import com.eatssu.common.UiState +import com.eatssu.common.enums.ToastType import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -65,11 +66,11 @@ class ModifyViewModel @Inject constructor( ) if (!success) { _uiState.value = UiState.Success(editing) - _uiEvent.emit(UiEvent.ShowToast("리뷰 수정이 실패했습니다.")) + _uiEvent.emit(UiEvent.ShowToast("리뷰 수정이 실패했습니다.", ToastType.ERROR)) } _uiEvent.emit(UiEvent.NavigateBack) - _uiEvent.emit(UiEvent.ShowToast("리뷰를 수정했습니다.")) + _uiEvent.emit(UiEvent.ShowToast("리뷰를 수정했습니다.", ToastType.SUCCESS)) } } } diff --git a/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/report/ReportActivity.kt b/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/report/ReportActivity.kt index dec710a88..ab5d3874a 100644 --- a/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/report/ReportActivity.kt +++ b/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/report/ReportActivity.kt @@ -10,6 +10,7 @@ import com.eatssu.android.presentation.base.BaseActivity import com.eatssu.android.presentation.util.showToast import com.eatssu.common.enums.ReportType import com.eatssu.common.enums.ScreenId +import com.eatssu.common.enums.ToastType import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @@ -68,7 +69,10 @@ class ReportActivity : BaseActivity( lifecycleScope.launch { reportViewModel.uiState.collectLatest { - showToast(it.toastMessage) + showToast( + it.toastMessage, + if (it.isDone) ToastType.SUCCESS else ToastType.ERROR + ) if (it.isDone) { finish() } diff --git a/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/write/WriteReviewScreen.kt b/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/write/WriteReviewScreen.kt index 5a718f610..554983cc8 100644 --- a/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/write/WriteReviewScreen.kt +++ b/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/write/WriteReviewScreen.kt @@ -81,7 +81,7 @@ fun WriteReviewScreen( when (event) { is UiEvent.NavigateBack -> onBack() is UiEvent.ShowToast -> { - context.showToast(event.message) + context.showToast(event) } } } diff --git a/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/write/WriteReviewViewModel.kt b/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/write/WriteReviewViewModel.kt index 10b524226..ef99e2278 100644 --- a/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/write/WriteReviewViewModel.kt +++ b/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/write/WriteReviewViewModel.kt @@ -11,6 +11,7 @@ import com.eatssu.android.domain.usecase.review.WriteReviewUseCase import com.eatssu.common.UiEvent import com.eatssu.common.UiState import com.eatssu.common.enums.MenuType +import com.eatssu.common.enums.ToastType import dagger.hilt.android.lifecycle.HiltViewModel import id.zelory.compressor.Compressor import kotlinx.coroutines.flow.MutableSharedFlow @@ -111,24 +112,24 @@ class WriteReviewViewModel @Inject constructor( val compressedFile = compressImage(context, originalFile) if (compressedFile != null && compressedFile.exists()) { imageUrl = getImageUrlUseCase(compressedFile) - _uiEvent.emit(UiEvent.ShowToast("이미지가 업로드되었습니다.")) + _uiEvent.emit(UiEvent.ShowToast("이미지가 업로드되었습니다.", ToastType.SUCCESS)) // 원본 파일 삭제 (압축된 파일만 유지) originalFile.delete() } else { _uiState.value = UiState.Success(editing) // 되돌림 - _uiEvent.emit(UiEvent.ShowToast("이미지 압축에 실패하였습니다.")) + _uiEvent.emit(UiEvent.ShowToast("이미지 압축에 실패하였습니다.", ToastType.ERROR)) return@launch } } else { _uiState.value = UiState.Success(editing) // 되돌림 - _uiEvent.emit(UiEvent.ShowToast("이미지 파일을 찾을 수 없습니다.")) + _uiEvent.emit(UiEvent.ShowToast("이미지 파일을 찾을 수 없습니다.", ToastType.ERROR)) return@launch } } catch (e: Exception) { Timber.e(e, "이미지 업로드 실패") _uiState.value = UiState.Success(editing) // 되돌림 - _uiEvent.emit(UiEvent.ShowToast("이미지 업로드에 실패하였습니다.")) + _uiEvent.emit(UiEvent.ShowToast("이미지 업로드에 실패하였습니다.", ToastType.ERROR)) return@launch } } @@ -145,11 +146,11 @@ class WriteReviewViewModel @Inject constructor( if (!success) { _uiState.value = UiState.Success(editing) // 되돌림 - _uiEvent.emit(UiEvent.ShowToast("리뷰 작성에 실패하였습니다.")) + _uiEvent.emit(UiEvent.ShowToast("리뷰 작성에 실패하였습니다.", ToastType.ERROR)) return@launch } - _uiEvent.emit(UiEvent.ShowToast("리뷰가 작성되었습니다.")) + _uiEvent.emit(UiEvent.ShowToast("리뷰가 작성되었습니다.", ToastType.SUCCESS)) _uiEvent.emit(UiEvent.NavigateBack) } } diff --git a/app/src/main/java/com/eatssu/android/presentation/common/AndroidMessageDialogActivity.kt b/app/src/main/java/com/eatssu/android/presentation/common/AndroidMessageDialogActivity.kt index b76ceb6d9..d4276c7ce 100644 --- a/app/src/main/java/com/eatssu/android/presentation/common/AndroidMessageDialogActivity.kt +++ b/app/src/main/java/com/eatssu/android/presentation/common/AndroidMessageDialogActivity.kt @@ -1,11 +1,9 @@ package com.eatssu.android.presentation.common - -import android.app.AlertDialog import android.os.Bundle import android.util.Log import androidx.appcompat.app.AppCompatActivity - +import com.eatssu.android.presentation.util.showDialog class AndroidMessageDialogActivity : AppCompatActivity() { @@ -15,23 +13,20 @@ class AndroidMessageDialogActivity : AppCompatActivity() { } private fun showDialog() { - val builder = AlertDialog.Builder(this) - - builder.setTitle("공지") val message = intent.getStringExtra("message") - Log.d("message",message.toString()) - builder.setMessage(intent.getStringExtra("message")) - - builder.setPositiveButton("확인") { dialog, which -> - // Google Play Store의 앱 페이지로 이동하여 업데이트를 다운로드합니다. - - // 다이얼로그를 종료합니다. - finish() + Log.d("message", message.toString()) + + showDialog( + title = "공지", + description = message ?: "" + ) { + confirmText = "확인" + showCancelButton = false + cancellable = false + onConfirm { dialog -> + dialog.dismiss() + finish() + } } - - builder.setCancelable(false) // 사용자가 다이얼로그를 취소할 수 없도록 설정 - - val dialog = builder.create() - dialog.show() } } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/presentation/common/ForceUpdateActivity.kt b/app/src/main/java/com/eatssu/android/presentation/common/ForceUpdateActivity.kt index 4e241bbaa..09ec36167 100644 --- a/app/src/main/java/com/eatssu/android/presentation/common/ForceUpdateActivity.kt +++ b/app/src/main/java/com/eatssu/android/presentation/common/ForceUpdateActivity.kt @@ -1,11 +1,11 @@ package com.eatssu.android.presentation.common -import android.app.AlertDialog import android.content.Intent -import android.net.Uri import android.os.Bundle import androidx.appcompat.app.AppCompatActivity +import androidx.core.net.toUri +import com.eatssu.android.presentation.util.showDialog class ForceUpdateDialogActivity : AppCompatActivity() { @@ -16,36 +16,33 @@ class ForceUpdateDialogActivity : AppCompatActivity() { } private fun showForceUpdateDialog() { - val builder = AlertDialog.Builder(this) - builder.setTitle("강제 업데이트") - builder.setMessage("새 버전의 앱을 설치해야 합니다.") - - builder.setPositiveButton("업데이트") { dialog, which -> - // Google Play Store의 앱 페이지로 이동하여 업데이트를 다운로드합니다. - val appPackageName = packageName - try { - startActivity( - Intent( - Intent.ACTION_VIEW, - Uri.parse("market://details?id=$appPackageName") + showDialog("강제 업데이트", "새 버전의 앱을 설치해야 합니다.") { + confirmText = "업데이트" + cancellable = false + showCancelButton = false + + onConfirm { + // Google Play Store의 앱 페이지로 이동하여 업데이트를 다운로드합니다. + val appPackageName = packageName + try { + startActivity( + Intent( + Intent.ACTION_VIEW, + "market://details?id=$appPackageName".toUri() + ) ) - ) - } catch (e: android.content.ActivityNotFoundException) { - startActivity( - Intent( - Intent.ACTION_VIEW, - Uri.parse("https://play.google.com/store/apps/details?id=$appPackageName") + } catch (e: android.content.ActivityNotFoundException) { + startActivity( + Intent( + Intent.ACTION_VIEW, + "https://play.google.com/store/apps/details?id=$appPackageName".toUri() + ) ) - ) - } + } - // 다이얼로그를 종료합니다. - finish() + // 다이얼로그를 종료합니다. + finish() + } } - - builder.setCancelable(false) // 사용자가 다이얼로그를 취소할 수 없도록 설정 - - val dialog = builder.create() - dialog.show() } } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/presentation/common/NetworkConnection.kt b/app/src/main/java/com/eatssu/android/presentation/common/NetworkConnection.kt index 522ba1f2c..af30eeae7 100644 --- a/app/src/main/java/com/eatssu/android/presentation/common/NetworkConnection.kt +++ b/app/src/main/java/com/eatssu/android/presentation/common/NetworkConnection.kt @@ -1,11 +1,14 @@ package com.eatssu.android.presentation.common -import android.app.AlertDialog +import android.app.Dialog import android.content.Context +import android.content.Intent import android.net.ConnectivityManager import android.net.Network import android.net.NetworkCapabilities import android.net.NetworkRequest +import android.provider.Settings +import com.eatssu.android.presentation.util.showDialog // 네트워크 연결 확인을 위해 네트워크 변경 시 알람에 사용하는 클래스 NetworkCallback 을 커스터마이징 class NetworkConnection(private val context: Context) : @@ -19,11 +22,21 @@ class NetworkConnection(private val context: Context) : .build() // 네트워크 연결 안 되어있을 때 보여줄 다이얼로그 - private val dialog: AlertDialog by lazy { - AlertDialog.Builder(context) - .setTitle("네트워크 연결 안 됨") - .setMessage("와이파이 또는 모바일 데이터를 확인해주세요") - .create() + private val dialog: Dialog by lazy { + context.showDialog("네트워크 연결 안 됨", "Wi-Fi, 모바일 데이터를 확인해주세요") { + cancellable = false + showCancelButton = false + showWhenStart = false + + onConfirm { + context.startActivity( + Intent(Settings.ACTION_WIRELESS_SETTINGS) + .apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + }) + it.dismiss() + } + } } // NetworkCallback 등록 diff --git a/app/src/main/java/com/eatssu/android/presentation/intro/IntroActivity.kt b/app/src/main/java/com/eatssu/android/presentation/intro/IntroActivity.kt index 08ad3a483..35cd4af3b 100644 --- a/app/src/main/java/com/eatssu/android/presentation/intro/IntroActivity.kt +++ b/app/src/main/java/com/eatssu/android/presentation/intro/IntroActivity.kt @@ -84,7 +84,7 @@ class IntroActivity : AppCompatActivity() { when (event) { is UiEvent.ShowToast -> { // 에러 메시지 표시 - showToast(event.message) + showToast(event) } } } diff --git a/app/src/main/java/com/eatssu/android/presentation/intro/IntroViewModel.kt b/app/src/main/java/com/eatssu/android/presentation/intro/IntroViewModel.kt index 8116f79b8..dc0304fce 100644 --- a/app/src/main/java/com/eatssu/android/presentation/intro/IntroViewModel.kt +++ b/app/src/main/java/com/eatssu/android/presentation/intro/IntroViewModel.kt @@ -1,7 +1,9 @@ package com.eatssu.android.presentation.intro +import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.eatssu.android.R import com.eatssu.android.BuildConfig.VERSION_CODE import com.eatssu.android.domain.repository.FirebaseRemoteConfigRepository import com.eatssu.android.domain.usecase.auth.GetAccessTokenUseCase @@ -9,7 +11,9 @@ import com.eatssu.android.domain.usecase.auth.GetIsAccessTokenValidUseCase import com.eatssu.android.domain.usecase.health.HealthCheckUseCase import com.eatssu.common.UiEvent import com.eatssu.common.UiState +import com.eatssu.common.enums.ToastType import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow @@ -21,6 +25,7 @@ import javax.inject.Inject @HiltViewModel class IntroViewModel @Inject constructor( + @ApplicationContext private val context: Context, private val healthCheckUseCase: HealthCheckUseCase, private val getAccessTokenUseCase: GetAccessTokenUseCase, private val getIsAccessTokenValidUseCase: GetIsAccessTokenValidUseCase, @@ -54,7 +59,7 @@ class IntroViewModel @Inject constructor( } catch (e: Exception) { Timber.e(e, "앱 초기화 중 오류 발생") _uiState.value = UiState.Error - _uiEvent.emit(UiEvent.ShowToast("앱 초기화 중 오류가 발생했습니다")) + _uiEvent.emit(UiEvent.ShowToast("앱 초기화 중 오류가 발생했습니다", ToastType.ERROR)) } } } @@ -78,7 +83,7 @@ class IntroViewModel @Inject constructor( when (result) { is VersionCheckResult.ForceUpdateRequired -> { Timber.d("강제 업데이트 필요: 최신 버전 ${result.minimumVersionCode}") - _uiEvent.emit(UiEvent.ShowToast("앱을 업데이트해주세요")) + _uiEvent.emit(UiEvent.ShowToast("앱을 업데이트해주세요", ToastType.INFO)) } VersionCheckResult.UpdateNotRequired -> { @@ -103,14 +108,24 @@ class IntroViewModel @Inject constructor( val userAccessToken = getAccessTokenUseCase() if (userAccessToken.isEmpty()) { _uiState.value = UiState.Error - _uiEvent.emit(UiEvent.ShowToast("로그인이 필요합니다")) + _uiEvent.emit( + UiEvent.ShowToast( + context.getString(R.string.toast_token_invalid), + ToastType.INFO + ) + ) return@launch } // 토큰이 있어도 유효하지 않음 if (!getIsAccessTokenValidUseCase(userAccessToken)) { _uiState.value = UiState.Error - _uiEvent.emit(UiEvent.ShowToast("로그인이 필요합니다")) + _uiEvent.emit( + UiEvent.ShowToast( + context.getString(R.string.toast_token_invalid), + ToastType.INFO + ) + ) return@launch } diff --git a/app/src/main/java/com/eatssu/android/presentation/login/LoginActivity.kt b/app/src/main/java/com/eatssu/android/presentation/login/LoginActivity.kt index e38971482..de593dfa0 100644 --- a/app/src/main/java/com/eatssu/android/presentation/login/LoginActivity.kt +++ b/app/src/main/java/com/eatssu/android/presentation/login/LoginActivity.kt @@ -10,6 +10,7 @@ import com.eatssu.android.R import com.eatssu.android.databinding.ActivityLoginBinding import com.eatssu.android.presentation.MainActivity import com.eatssu.android.presentation.base.BaseActivity +import com.eatssu.android.presentation.util.showErrorToast import com.eatssu.android.presentation.util.showToast import com.eatssu.android.presentation.util.startActivity import com.eatssu.common.UiEvent @@ -86,7 +87,7 @@ class LoginActivity : else -> { Timber.e(error, "Login failed") - showToast(getString(R.string.login_failed)) + showErrorToast(R.string.toast_login_failed) } } } @@ -115,7 +116,7 @@ class LoginActivity : repeatOnLifecycle(Lifecycle.State.STARTED) { loginViewModel.uiEvent.collect { event -> when (event) { - is UiEvent.ShowToast -> showToast(event.message) + is UiEvent.ShowToast -> showToast(event) } } } diff --git a/app/src/main/java/com/eatssu/android/presentation/login/LoginViewModel.kt b/app/src/main/java/com/eatssu/android/presentation/login/LoginViewModel.kt index fd0d1f2cb..662be5580 100644 --- a/app/src/main/java/com/eatssu/android/presentation/login/LoginViewModel.kt +++ b/app/src/main/java/com/eatssu/android/presentation/login/LoginViewModel.kt @@ -12,6 +12,7 @@ import com.eatssu.android.domain.usecase.auth.SetRefreshTokenUseCase import com.eatssu.android.domain.usecase.user.SetUserEmailUseCase import com.eatssu.common.UiEvent import com.eatssu.common.UiState +import com.eatssu.common.enums.ToastType import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers @@ -45,7 +46,12 @@ class LoginViewModel @Inject constructor( val token = loginUseCase(LoginWithKakaoRequest(email, providerID)) ?: run { _uiState.value = UiState.Error - _uiEvent.emit(UiEvent.ShowToast(context.getString(R.string.login_failed))) + _uiEvent.emit( + UiEvent.ShowToast( + context.getString(R.string.toast_login_failed), + ToastType.ERROR + ) + ) return@launch } 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 aaea16688..120f3ac70 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 @@ -9,7 +9,6 @@ import android.os.Bundle import android.provider.Settings 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 @@ -25,6 +24,9 @@ import com.eatssu.android.presentation.login.LoginActivity import com.eatssu.android.presentation.mypage.myreview.MyReviewListComposeActivity import com.eatssu.android.presentation.mypage.terms.WebViewActivity import com.eatssu.android.presentation.mypage.userinfo.UserInfoActivity +import com.eatssu.android.presentation.util.showDialog +import com.eatssu.android.presentation.util.showErrorToast +import com.eatssu.android.presentation.util.showInfoToast import com.eatssu.android.presentation.util.showToast import com.eatssu.common.UiEvent import com.eatssu.common.UiState @@ -72,7 +74,7 @@ class MyPageFragment : BaseFragment(ScreenId.MYPAGE_MAIN) } is UiState.Error -> { - showToast(getString(R.string.not_found)) + showErrorToast(R.string.not_found) } } } @@ -80,8 +82,7 @@ class MyPageFragment : BaseFragment(ScreenId.MYPAGE_MAIN) launch { myPageViewModel.uiEvent.collectLatest { event -> when (event) { - is UiEvent.ShowToast -> showToast(event.message) - else -> Unit + is UiEvent.ShowToast -> showToast(event) } } } @@ -116,7 +117,7 @@ class MyPageFragment : BaseFragment(ScreenId.MYPAGE_MAIN) if (isChecked) { if (checkNotificationPermission(requireContext())) { myPageViewModel.setNotificationOn() - showToast("EAT-SSU 알림 수신을 동의하였습니다.\n$formattedDate") + showInfoToast(getString(R.string.toast_notification_enable, formattedDate)) } else { showNotificationPermissionDialog() // 권한 미허용이면 스위치 원복 @@ -128,7 +129,7 @@ class MyPageFragment : BaseFragment(ScreenId.MYPAGE_MAIN) } } else { myPageViewModel.setNotificationOff() - showToast("EAT-SSU 알림 수신을 거부하였습니다.\n$formattedDate") + showInfoToast(getString(R.string.toast_notification_disable, formattedDate)) } } @@ -188,14 +189,19 @@ class MyPageFragment : BaseFragment(ScreenId.MYPAGE_MAIN) } private fun showNotificationPermissionDialog() { - AlertDialog.Builder(requireContext()) - .setTitle("알림 권한 필요") - .setMessage("알림을 받으려면 알림 권한을 활성화해야 합니다. 설정 화면으로 이동하시겠습니까?") - .setPositiveButton("설정으로 이동") { _, _ -> - openAppNotificationSettings(requireContext()) + requireContext().run { + showDialog( + title = "알림 권한 필요", + description = "알림을 받으려면 알림 권한을 활성화해야 합니다. 설정 화면으로 이동하시겠습니까?" + ) { + confirmText = "설정으로 이동" + cancelText = "취소" + onConfirm { dialog -> + openAppNotificationSettings(this@run) + dialog.dismiss() + } } - .setNegativeButton("취소", null) - .show() + } } private fun checkNotificationPermission(context: Context): Boolean { @@ -208,22 +214,22 @@ class MyPageFragment : BaseFragment(ScreenId.MYPAGE_MAIN) } private fun showLogoutDialog() { - AlertDialog.Builder(requireContext()) - .setTitle("로그아웃") - .setMessage("로그아웃 하시겠습니까?") - .setPositiveButton("로그아웃") { _, _ -> - mainViewModel.logOut() // 로그아웃은 메인 액티비티에서 처리하도록 수정 - startActivity(Intent(requireContext(), LoginActivity::class.java)) + requireContext().run { + showDialog("로그아웃", "로그아웃 하시겠습니까?") { + isDestructive = true + onConfirm { + mainViewModel.logOut() // 로그아웃은 메인 액티비티에서 처리하도록 수정 + startActivity(Intent(this@run, LoginActivity::class.java)) + } } - .setNegativeButton("취소", null) - .show() + } } private fun moveToOss() { try { startActivity(Intent(requireContext(), OssLicensesMenuActivity::class.java)) } catch (e: Exception) { - showToast("오픈소스 라이브러리를 불러올 수 없습니다.") + showErrorToast(getString(R.string.toast_oss_load_fail)) Timber.e("Error opening OSS Licenses: ${e.message}") } } 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 8958e20b5..50e1c6cf0 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 @@ -1,15 +1,19 @@ package com.eatssu.android.presentation.mypage +import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.eatssu.android.BuildConfig +import com.eatssu.android.R import com.eatssu.android.data.local.SettingDataStore 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.common.UiEvent import com.eatssu.common.UiState +import com.eatssu.common.enums.ToastType import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow @@ -24,6 +28,7 @@ import javax.inject.Inject @HiltViewModel class MyPageViewModel @Inject constructor( + @ApplicationContext private val context: Context, private val getUserNickNameUseCase: GetUserNickNameUseCase, private val setNotificationStatusUseCase: SetDailyNotificationStatusUseCase, private val alarmUseCase: AlarmUseCase, @@ -69,7 +74,12 @@ class MyPageViewModel @Inject constructor( if (nickname.isBlank()) { _state.update { it.copy(nickname = null) } - _uiEvent.emit(UiEvent.ShowToast("닉네임을 설정해주세요.")) + _uiEvent.emit( + UiEvent.ShowToast( + context.getString(R.string.toast_require_nickname), + ToastType.INFO + ) + ) return@launch } diff --git a/app/src/main/java/com/eatssu/android/presentation/mypage/SignOutActivity.kt b/app/src/main/java/com/eatssu/android/presentation/mypage/SignOutActivity.kt index 4f0ce55ca..1d50cfbc6 100644 --- a/app/src/main/java/com/eatssu/android/presentation/mypage/SignOutActivity.kt +++ b/app/src/main/java/com/eatssu/android/presentation/mypage/SignOutActivity.kt @@ -72,9 +72,7 @@ class SignOutActivity : lifecycleScope.launch { signOutViewModel.uiEvent.collectLatest { event -> when (event) { - is UiEvent.ShowToast -> { - showToast(event.message) - } + is UiEvent.ShowToast -> showToast(event) } } } diff --git a/app/src/main/java/com/eatssu/android/presentation/mypage/SignOutViewModel.kt b/app/src/main/java/com/eatssu/android/presentation/mypage/SignOutViewModel.kt index 867782933..74e4edb45 100644 --- a/app/src/main/java/com/eatssu/android/presentation/mypage/SignOutViewModel.kt +++ b/app/src/main/java/com/eatssu/android/presentation/mypage/SignOutViewModel.kt @@ -1,12 +1,16 @@ package com.eatssu.android.presentation.mypage +import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.eatssu.android.R import com.eatssu.android.domain.usecase.auth.LogoutUseCase import com.eatssu.android.domain.usecase.auth.SignOutUseCase import com.eatssu.common.UiEvent import com.eatssu.common.UiState +import com.eatssu.common.enums.ToastType import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow @@ -17,6 +21,7 @@ import javax.inject.Inject @HiltViewModel class SignOutViewModel @Inject constructor( + @ApplicationContext private val context: Context, private val logoutUseCase: LogoutUseCase, private val signOutUseCase: SignOutUseCase, ) : ViewModel() { @@ -34,12 +39,22 @@ class SignOutViewModel @Inject constructor( val success = signOutUseCase() if (!success) { _uiState.value = UiState.Error - _uiEvent.emit(UiEvent.ShowToast("탈퇴에 실패했습니다.")) + _uiEvent.emit( + UiEvent.ShowToast( + context.getString(R.string.toast_sign_out_fail), + ToastType.ERROR + ) + ) return@launch } _uiState.value = UiState.Success(SignOutState(isSignOuted = true)) - _uiEvent.emit(UiEvent.ShowToast("탈퇴가 완료되었습니다.")) + _uiEvent.emit( + UiEvent.ShowToast( + context.getString(R.string.toast_sign_out_success), + ToastType.SUCCESS + ) + ) logoutUseCase() // 자동 로그인 정보 삭제 } } diff --git a/app/src/main/java/com/eatssu/android/presentation/mypage/myreview/MyReviewListScreen.kt b/app/src/main/java/com/eatssu/android/presentation/mypage/myreview/MyReviewListScreen.kt index 458100308..3fbb9d2ff 100644 --- a/app/src/main/java/com/eatssu/android/presentation/mypage/myreview/MyReviewListScreen.kt +++ b/app/src/main/java/com/eatssu/android/presentation/mypage/myreview/MyReviewListScreen.kt @@ -63,7 +63,7 @@ fun MyReviewListScreen( when (uiEvent) { is UiEvent.ShowToast -> { - context.showToast((uiEvent as UiEvent.ShowToast).message) + context.showToast(uiEvent as UiEvent.ShowToast) } } diff --git a/app/src/main/java/com/eatssu/android/presentation/mypage/myreview/MyReviewViewModel.kt b/app/src/main/java/com/eatssu/android/presentation/mypage/myreview/MyReviewViewModel.kt index d020619c2..53cfe2a60 100644 --- a/app/src/main/java/com/eatssu/android/presentation/mypage/myreview/MyReviewViewModel.kt +++ b/app/src/main/java/com/eatssu/android/presentation/mypage/myreview/MyReviewViewModel.kt @@ -8,6 +8,7 @@ import com.eatssu.android.domain.usecase.review.GetMyReviewsUseCase import com.eatssu.android.domain.usecase.user.GetUserNickNameUseCase import com.eatssu.common.UiEvent import com.eatssu.common.UiState +import com.eatssu.common.enums.ToastType import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -64,10 +65,10 @@ class MyReviewViewModel @Inject constructor( viewModelScope.launch { val success = deleteReviewUseCase(reviewId) if (!success) { - _uiEvent.emit(UiEvent.ShowToast("리뷰 삭제에 실패했습니다.")) + _uiEvent.emit(UiEvent.ShowToast("리뷰 삭제에 실패했습니다.", ToastType.ERROR)) return@launch } - _uiEvent.emit(UiEvent.ShowToast("리뷰를 삭제했습니다.")) + _uiEvent.emit(UiEvent.ShowToast("리뷰를 삭제했습니다.", ToastType.SUCCESS)) // 삭제 성공 시 내 리뷰 목록 재조회 getMyReviewList() } diff --git a/app/src/main/java/com/eatssu/android/presentation/mypage/userinfo/UserInfoActivity.kt b/app/src/main/java/com/eatssu/android/presentation/mypage/userinfo/UserInfoActivity.kt index 9804db0b5..e6a672a41 100644 --- a/app/src/main/java/com/eatssu/android/presentation/mypage/userinfo/UserInfoActivity.kt +++ b/app/src/main/java/com/eatssu/android/presentation/mypage/userinfo/UserInfoActivity.kt @@ -19,6 +19,7 @@ import com.eatssu.android.presentation.util.showToast import com.eatssu.common.UiEvent import com.eatssu.common.UiState import com.eatssu.common.enums.ScreenId +import com.eatssu.common.enums.ToastType import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @@ -91,7 +92,7 @@ class UserInfoActivity : lifecycleScope.launch { viewModel.uiEvent.collectLatest { event -> when (event) { - is UiEvent.ShowToast -> showToast(event.message) + is UiEvent.ShowToast -> showToast(event) } } } @@ -177,7 +178,7 @@ class UserInfoActivity : // 단과대를 먼저 선택하도록 유도 if (data.selectedCollege.collegeId == -1) { - showToast("단과대를 먼저 선택해 주세요.") + showToast("단과대를 먼저 선택해 주세요.", ToastType.ERROR) return } 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 6c4260642..482979fa5 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 @@ -13,6 +13,7 @@ import com.eatssu.android.domain.usecase.user.ValidateNicknameLocalUseCase import com.eatssu.android.domain.usecase.user.ValidateNicknameServerUseCase import com.eatssu.common.UiEvent import com.eatssu.common.UiState +import com.eatssu.common.enums.ToastType import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -206,7 +207,7 @@ class UserInfoViewModel @Inject constructor( val result = setUserNicknameUseCase(data.nickname) result.onFailure { error -> val errorMessage = error.message ?: "닉네임 변경에 실패했어요." - _uiEvent.emit(UiEvent.ShowToast(errorMessage)) + _uiEvent.emit(UiEvent.ShowToast(errorMessage, ToastType.ERROR)) _uiState.value = UiState.Error return@launch } @@ -236,7 +237,7 @@ class UserInfoViewModel @Inject constructor( else -> "변경사항이 없습니다." } - _uiEvent.emit(UiEvent.ShowToast(message)) + _uiEvent.emit(UiEvent.ShowToast(message, ToastType.INFO)) _uiState.update { UiState.Success( data.copy( diff --git a/app/src/main/java/com/eatssu/android/presentation/util/ActivityUtil.kt b/app/src/main/java/com/eatssu/android/presentation/util/ActivityUtil.kt index af9dee9fe..915f01523 100644 --- a/app/src/main/java/com/eatssu/android/presentation/util/ActivityUtil.kt +++ b/app/src/main/java/com/eatssu/android/presentation/util/ActivityUtil.kt @@ -1,13 +1,13 @@ package com.eatssu.android.presentation.util import android.app.Activity -import android.app.AlertDialog import android.content.Intent import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import com.eatssu.android.R import com.eatssu.android.presentation.base.NetworkErrorEventBus import kotlinx.coroutines.launch +import java.util.concurrent.atomic.AtomicBoolean inline fun AppCompatActivity.startActivity(block: Intent.() -> Unit = {}) { startActivity(Intent(this, T::class.java).apply(block)) @@ -21,31 +21,23 @@ fun AppCompatActivity.observeNetworkError( errorTitle: String? = null, errorMessage: String? = null ) { - var networkErrorDialog: AlertDialog? = null + val isShowingDialog = AtomicBoolean(false) lifecycleScope.launch { NetworkErrorEventBus.networkError.collect { - if (networkErrorDialog?.isShowing == true) { - return@collect - } + // atomic하게 false→true로 변경, 이미 true면 false 반환 + if (!isShowingDialog.compareAndSet(false, true)) return@collect val title = errorTitle ?: getString(R.string.server_error_title) val message = errorMessage ?: getString(R.string.server_error_message) - networkErrorDialog = AlertDialog.Builder(this@observeNetworkError) - .setTitle(title) - .setMessage(message) - .setPositiveButton(getString(R.string.confirm)) { dialog, _ -> - dialog.dismiss() - } - .setCancelable(true) - .create() - .also { dialog -> - dialog.setOnDismissListener { - networkErrorDialog = null - } - dialog.show() + showDialog(title, message) { + showCancelButton = false + onConfirm { it.dismiss() } + onDismiss { + isShowingDialog.set(false) } + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/presentation/util/ContextUtil.kt b/app/src/main/java/com/eatssu/android/presentation/util/ContextUtil.kt deleted file mode 100644 index 492a0fb37..000000000 --- a/app/src/main/java/com/eatssu/android/presentation/util/ContextUtil.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.eatssu.android.presentation.util - -import android.content.Context -import android.widget.Toast -import androidx.fragment.app.Fragment - -// Activity -fun Context.showToast(msg: String) { - if (msg.isNotEmpty()) { - Toast.makeText(this, msg, Toast.LENGTH_SHORT).show() - } -} - -// Fragment -fun Fragment.showToast(msg: String) { - if (msg.isNotEmpty()) { - Toast.makeText(requireContext(), msg, Toast.LENGTH_SHORT).show() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/presentation/util/DialogUtil.kt b/app/src/main/java/com/eatssu/android/presentation/util/DialogUtil.kt new file mode 100644 index 000000000..9b9cf72e7 --- /dev/null +++ b/app/src/main/java/com/eatssu/android/presentation/util/DialogUtil.kt @@ -0,0 +1,121 @@ +package com.eatssu.android.presentation.util + +import android.app.Dialog +import android.content.Context +import android.graphics.Color +import android.view.Window +import android.widget.Button +import android.widget.TextView +import androidx.annotation.StringRes +import androidx.core.graphics.drawable.toDrawable +import com.eatssu.android.R + +class DialogBuilder( + private val context: Context, + + // Dialog에 표시할 제목 + private val title: String, + + // Dialog에 표시할 설명 + private val description: String +) { + // 확인 버튼을 눌렀을 때 동작 + private var onConfirm: (dialog: Dialog) -> Unit = { it.dismiss() } + + // 취소 버튼을 눌렀을 때 동작 + private var onCancel: (dialog: Dialog) -> Unit = { it.dismiss() } + + // Dialog가 닫힐 때 동작 + private var onDismiss: () -> Unit = {} + + // Dialog 바깥을 누르면 닫히는지 여부 + var cancellable: Boolean = true + + // 확인 버튼 텍스트 + var confirmText: String = context.getString(R.string.confirm) + + // 취소 버튼 텍스트 + var cancelText: String = context.getString(R.string.cancel) + + // 취소 버튼 표시 여부 + var showCancelButton: Boolean = true + + // Destructive한 동작인지 여부 - 확인/취소 버튼 위치 변경, 확인 색 Error 색 변경 + var isDestructive: Boolean = false + + // Dialog를 생성 후 바로 열 것인지 여부 + var showWhenStart: Boolean = true + + fun onConfirm(action: (dialog: Dialog) -> Unit) = apply { + this.onConfirm = action + } + + fun onCancel(action: (dialog: Dialog) -> Unit) = apply { + this.onCancel = action + } + + fun onDismiss(action: () -> Unit) = apply { + this.onDismiss = action + } + + fun show(): Dialog { + val dialog = Dialog(context) + + // Dialog Radius 적용 + dialog.window?.setBackgroundDrawable(Color.TRANSPARENT.toDrawable()) + dialog.requestWindowFeature(Window.FEATURE_NO_TITLE) + dialog.setContentView(if (isDestructive) R.layout.dialog_destructive else R.layout.dialog_default) + + // UI 설정 + dialog.findViewById(R.id.dialog_title).text = title + dialog.findViewById(R.id.dialog_description).text = description + + val confirmButton = dialog.findViewById