From f047e2db4eed2885229bfbe6db44730eed4db3a8 Mon Sep 17 00:00:00 2001 From: juhwankim-dev Date: Sat, 30 May 2026 16:20:59 +0900 Subject: [PATCH 1/6] =?UTF-8?q?NR-144=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EB=A5=BC=20compose=EB=A1=9C=20=EB=A7=88?= =?UTF-8?q?=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/mypage/MypageFragment.kt | 114 +++++---- .../presentation/ui/mypage/MypageScreen.kt | 213 ++++++++++++++++ .../presentation/ui/mypage/MypageViewModel.kt | 13 +- .../src/main/res/layout/fragment_mypage.xml | 237 ------------------ .../main/res/navigation/mypage_navigation.xml | 3 +- 5 files changed, 281 insertions(+), 299 deletions(-) create mode 100644 presentation/src/main/java/com/nextroom/nextroom/presentation/ui/mypage/MypageScreen.kt delete mode 100644 presentation/src/main/res/layout/fragment_mypage.xml diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/mypage/MypageFragment.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/mypage/MypageFragment.kt index 8443e37d..c1d5d507 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/mypage/MypageFragment.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/mypage/MypageFragment.kt @@ -3,17 +3,22 @@ package com.nextroom.nextroom.presentation.ui.mypage import android.content.Intent import android.net.Uri import android.os.Bundle +import android.view.LayoutInflater import android.view.View -import androidx.core.view.isVisible +import android.view.ViewGroup +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.setFragmentResultListener import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import com.nextroom.nextroom.domain.model.SubscribeStatus import com.nextroom.nextroom.presentation.NavGraphDirections import com.nextroom.nextroom.presentation.R -import com.nextroom.nextroom.presentation.base.BaseFragment +import com.nextroom.nextroom.presentation.base.ComposeBaseFragment import com.nextroom.nextroom.presentation.common.NRTwoButtonDialog -import com.nextroom.nextroom.presentation.databinding.FragmentMypageBinding +import com.nextroom.nextroom.presentation.common.compose.NRLoading import com.nextroom.nextroom.presentation.extension.repeatOnStarted import com.nextroom.nextroom.presentation.extension.safeNavigate import com.nextroom.nextroom.presentation.extension.snackbar @@ -22,67 +27,49 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch @AndroidEntryPoint -class MypageFragment : BaseFragment(FragmentMypageBinding::inflate) { +class MypageFragment : ComposeBaseFragment() { - private val viewModel: MypageViewModel by viewModels() - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + override val screenName: String = "mypage" - initViews() - initListeners() - initObserve() - setFragmentResultListeners() - } + private val viewModel: MypageViewModel by viewModels() - private fun initViews() = with(binding) { - tbMypage.apply { - tvButton.isVisible = false - tvTitle.text = getString(R.string.mypage_title) - } - } + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + return ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + val uiState by viewModel.uiState.collectAsState() + when (val state = uiState) { + is MypageViewModel.UiState.Loaded -> { + MypageScreen( + state = state, + onBackClick = { findNavController().popBackStack() }, + onSubscribeClick = ::onSubscribeClick, + onChangeAppPasswordClick = ::moveToSetPassword, + onCustomerServiceClick = ::openCustomerService, + onLogoutClick = viewModel::logout, + onResignClick = ::showConfirmResignDialog, + ) + } - private fun initListeners() = with(binding) { - tbMypage.ivBack.setOnClickListener { findNavController().popBackStack() } - tvLogoutButton.setOnClickListener { viewModel.logout() } - tvResignButton.setOnClickListener { showConfirmResignDialog() } - clSubscribe.setOnClickListener { - (viewModel.uiState.value as? MypageViewModel.UiState.Loaded)?.let { loaded -> - when (loaded.status) { - SubscribeStatus.SUBSCRIPTION_EXPIRATION, - SubscribeStatus.Default -> goToPurchase() - - SubscribeStatus.Subscribed -> goToSubscriptionInfo() + MypageViewModel.UiState.Failure, + MypageViewModel.UiState.Loading -> NRLoading(true) } } } - clChangeAppPassword.setOnClickListener { - moveToSetPassword() - } - clCustomerService.setOnClickListener { - try { - getString(R.string.link_official_instagram).let { url -> - Intent(Intent.ACTION_VIEW).apply { data = Uri.parse(url) } - }.also { startActivity(it) } - } catch (e: Exception) { - toast(getString(R.string.error_something)) - } - } } - private fun initObserve() { + override fun initObserve() { + super.initObserve() + viewLifecycleOwner.repeatOnStarted { launch { viewModel.uiState.collect { state -> - when (state) { - MypageViewModel.UiState.Failure -> snackbar(R.string.error_something) - is MypageViewModel.UiState.Loaded -> { - binding.tvShopName.text = state.shopName - binding.pbLoading.isVisible = false - binding.tvAppVersion.text = state.appVersion - } - - MypageViewModel.UiState.Loading -> binding.pbLoading.isVisible = true + if (state is MypageViewModel.UiState.Failure) { + snackbar(R.string.error_something) } } } @@ -97,12 +84,33 @@ class MypageFragment : BaseFragment(FragmentMypageBinding } } - private fun setFragmentResultListeners() { + override fun setFragmentResultListeners() { setFragmentResultListener(REQUEST_KEY_RESIGN) { _, _ -> viewModel.resign() } } + private fun onSubscribeClick() { + val loaded = viewModel.uiState.value as? MypageViewModel.UiState.Loaded ?: return + when (loaded.status) { + SubscribeStatus.SUBSCRIPTION_EXPIRATION, + SubscribeStatus.Default -> goToPurchase() + + SubscribeStatus.Subscribed -> goToSubscriptionInfo() + } + } + + private fun openCustomerService() { + try { + val intent = Intent(Intent.ACTION_VIEW).apply { + data = Uri.parse(getString(R.string.link_official_instagram)) + } + startActivity(intent) + } catch (e: Exception) { + toast(getString(R.string.error_something)) + } + } + private fun goToPurchase() { val action = NavGraphDirections.moveToPurchaseFragment() findNavController().safeNavigate(action) diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/mypage/MypageScreen.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/mypage/MypageScreen.kt new file mode 100644 index 00000000..f789973e --- /dev/null +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/mypage/MypageScreen.kt @@ -0,0 +1,213 @@ +package com.nextroom.nextroom.presentation.ui.mypage + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.nextroom.nextroom.domain.model.SubscribeStatus +import com.nextroom.nextroom.presentation.R +import com.nextroom.nextroom.presentation.common.compose.NRColor +import com.nextroom.nextroom.presentation.common.compose.NRToolbar +import com.nextroom.nextroom.presentation.common.compose.NRTypo +import com.nextroom.nextroom.presentation.extension.throttleClick + +@Composable +fun MypageScreen( + state: MypageViewModel.UiState.Loaded, + onBackClick: () -> Unit, + onSubscribeClick: () -> Unit, + onChangeAppPasswordClick: () -> Unit, + onCustomerServiceClick: () -> Unit, + onLogoutClick: () -> Unit, + onResignClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .fillMaxSize() + .background(NRColor.Dark01), + ) { + Column(modifier = Modifier.fillMaxSize()) { + NRToolbar( + title = stringResource(R.string.mypage_title), + onBackClick = onBackClick, + modifier = Modifier.fillMaxWidth(), + ) + + Text( + text = stringResource(R.string.admin_main_shop_name_label), + style = NRTypo.Pretendard.size14SemiBold, + color = NRColor.White, + modifier = Modifier.padding(start = 20.dp, top = 12.dp), + ) + + Text( + text = state.shopName, + style = NRTypo.Pretendard.size24, + color = NRColor.White, + modifier = Modifier.padding(start = 20.dp, top = 8.dp), + ) + + Spacer(modifier = Modifier.height(20.dp)) + + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + .height(1.dp) + .background(NRColor.Gray02), + ) + + Spacer(modifier = Modifier.height(16.dp)) + + MypageMenuRow( + title = stringResource(R.string.subscribe), + onClick = onSubscribeClick, + ) + + MypageMenuRow( + title = stringResource(R.string.text_change_app_password), + onClick = onChangeAppPasswordClick, + ) + + MypageMenuRow( + title = stringResource(R.string.text_customer_service), + onClick = onCustomerServiceClick, + ) + + MypageAppVersionRow(appVersion = state.appVersion) + + Spacer(modifier = Modifier.weight(1f)) + + MypageBottomActions( + onLogoutClick = onLogoutClick, + onResignClick = onResignClick, + ) + } + } +} + +@Composable +private fun MypageMenuRow( + title: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .fillMaxWidth() + .throttleClick { onClick() } + .padding(horizontal = 20.dp, vertical = 13.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = title, + style = NRTypo.Pretendard.size16Bold, + color = NRColor.White, + modifier = Modifier.weight(1f), + ) + Image( + painter = painterResource(R.drawable.ic_navigate_next), + contentDescription = null, + modifier = Modifier.size(24.dp), + ) + } +} + +@Composable +private fun MypageAppVersionRow( + appVersion: String, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 13.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(R.string.text_app_version), + style = NRTypo.Pretendard.size16Bold, + color = NRColor.White, + modifier = Modifier.weight(1f), + ) + Text( + text = appVersion, + style = NRTypo.Pretendard.size16, + color = NRColor.White50, + ) + } +} + +@Composable +private fun MypageBottomActions( + onLogoutClick: () -> Unit, + onResignClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(R.string.logout_button), + style = NRTypo.Pretendard.size14, + color = NRColor.White50, + modifier = Modifier + .throttleClick { onLogoutClick() } + .padding(16.dp), + ) + Box( + modifier = Modifier + .width(1.dp) + .height(10.dp) + .background(NRColor.White50), + ) + Text( + text = stringResource(R.string.text_user_resign), + style = NRTypo.Pretendard.size14, + color = NRColor.White50, + modifier = Modifier + .throttleClick { onResignClick() } + .padding(16.dp), + ) + } +} + +@Preview(showBackground = true, backgroundColor = 0xFF151516) +@Composable +private fun MypageScreenPreview() { + MypageScreen( + state = MypageViewModel.UiState.Loaded( + shopName = "비트포비아 강남 2호점", + status = SubscribeStatus.Subscribed, + appVersion = "1.4.7", + ), + onBackClick = {}, + onSubscribeClick = {}, + onChangeAppPasswordClick = {}, + onCustomerServiceClick = {}, + onLogoutClick = {}, + onResignClick = {}, + ) +} diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/mypage/MypageViewModel.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/mypage/MypageViewModel.kt index 43517fac..627443ee 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/mypage/MypageViewModel.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/mypage/MypageViewModel.kt @@ -1,12 +1,11 @@ package com.nextroom.nextroom.presentation.ui.mypage -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import com.nextroom.nextroom.domain.model.SubscribeStatus import com.nextroom.nextroom.domain.model.onFailure import com.nextroom.nextroom.domain.model.onFinally import com.nextroom.nextroom.domain.model.onSuccess import com.nextroom.nextroom.domain.repository.AdminRepository +import com.nextroom.nextroom.presentation.base.NewBaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -22,7 +21,7 @@ import javax.inject.Named class MypageViewModel @Inject constructor( private val adminRepository: AdminRepository, @Named("app_version") private val appVersion: String, -) : ViewModel() { +) : NewBaseViewModel() { private val _myInfo = MutableStateFlow(UiState.Loading) private val _isResignLoading = MutableStateFlow(false) @@ -36,7 +35,7 @@ class MypageViewModel @Inject constructor( } else { myInfo } - }.stateIn(viewModelScope, SharingStarted.Lazily, UiState.Loading) + }.stateIn(baseViewModelScope, SharingStarted.Lazily, UiState.Loading) private val _uiEvent = MutableSharedFlow() val uiEvent = _uiEvent.asSharedFlow() @@ -46,13 +45,13 @@ class MypageViewModel @Inject constructor( } fun logout() { - viewModelScope.launch { + baseViewModelScope.launch { adminRepository.logout() } } private fun fetchMyInfo() { - viewModelScope.launch { + baseViewModelScope.launch { adminRepository.getUserSubscribe().onSuccess { mypage -> UiState.Loaded( shopName = mypage.name, @@ -68,7 +67,7 @@ class MypageViewModel @Inject constructor( } fun resign() { - viewModelScope.launch { + baseViewModelScope.launch { _isResignLoading.emit(true) adminRepository.resign().onSuccess { _uiEvent.emit(UiEvent.ResignSuccess) diff --git a/presentation/src/main/res/layout/fragment_mypage.xml b/presentation/src/main/res/layout/fragment_mypage.xml deleted file mode 100644 index 10270660..00000000 --- a/presentation/src/main/res/layout/fragment_mypage.xml +++ /dev/null @@ -1,237 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/navigation/mypage_navigation.xml b/presentation/src/main/res/navigation/mypage_navigation.xml index 387063f0..bf809657 100644 --- a/presentation/src/main/res/navigation/mypage_navigation.xml +++ b/presentation/src/main/res/navigation/mypage_navigation.xml @@ -7,8 +7,7 @@ + android:name="com.nextroom.nextroom.presentation.ui.mypage.MypageFragment"> From 8a6356c63aae54a3c36016732e53ecbb9ccc3187 Mon Sep 17 00:00:00 2001 From: juhwankim-dev Date: Sat, 30 May 2026 16:43:34 +0900 Subject: [PATCH 2/6] =?UTF-8?q?NR-144=20=EC=97=90=EB=9F=AC=20=ED=8C=9D?= =?UTF-8?q?=EC=97=85=EC=9D=98=20=ED=99=95=EC=9D=B8=20=EB=B2=84=ED=8A=BC?= =?UTF-8?q?=EC=9D=84=20=EB=88=84=EB=A5=B8=20=EB=92=A4=20=EB=8F=99=EC=9E=91?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 왜? 화면을 그릴 데이터를 로드하다가 에러가 나는 경우 보여줄 fail 화면이 있으면 모르겠지만... 없는 케이스가 있어서 그런 경우는 확인 버튼을 눌렀을때 이전 화면으로 보내는 것이 자연스러워서 이런 로직을 추가함 --- .../base/BaseViewModelFragment.kt | 23 ++++++++++++++++--- .../base/ComposeBaseViewModelFragment.kt | 23 ++++++++++++++++--- .../presentation/base/NewBaseViewModel.kt | 12 ++++++---- 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/base/BaseViewModelFragment.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/base/BaseViewModelFragment.kt index 28ddb2b4..1567bf7c 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/base/BaseViewModelFragment.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/base/BaseViewModelFragment.kt @@ -1,5 +1,6 @@ package com.nextroom.nextroom.presentation.base +import androidx.fragment.app.setFragmentResultListener import androidx.navigation.fragment.findNavController import androidx.viewbinding.ViewBinding import com.nextroom.nextroom.presentation.NavGraphDirections @@ -13,22 +14,38 @@ abstract class BaseViewModelFragment(pr NewBaseFragment(inflate) { abstract val viewModel: VM + private var pendingErrorAction: NewBaseViewModel.ErrorAction = NewBaseViewModel.ErrorAction.STAY + override fun initObserve() { super.initObserve() + setFragmentResultListener(ERROR_DIALOG_KEY) { _, _ -> + val action = pendingErrorAction + pendingErrorAction = NewBaseViewModel.ErrorAction.STAY + if (action == NewBaseViewModel.ErrorAction.POP_BACK_STACK) { + findNavController().popBackStack() + } + } + viewLifecycleOwner.repeatOnStarted { launch { - viewModel.errorFlow.collect { + viewModel.errorFlow.collect { event -> + pendingErrorAction = event.action NavGraphDirections.moveToNrOneButtonDialog( NROneButtonDialog.NROneButtonArgument( title = getString(R.string.dialog_noti), message = getString(R.string.error_something), btnText = getString(R.string.text_confirm), - errorText = it.message, + errorText = event.throwable.message, + dialogKey = ERROR_DIALOG_KEY, ) ).also { findNavController().safeNavigate(it) } } } } } -} \ No newline at end of file + + companion object { + private const val ERROR_DIALOG_KEY = "base_view_model_error_dialog" + } +} diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/base/ComposeBaseViewModelFragment.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/base/ComposeBaseViewModelFragment.kt index 5b41dca8..4950ccea 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/base/ComposeBaseViewModelFragment.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/base/ComposeBaseViewModelFragment.kt @@ -1,5 +1,6 @@ package com.nextroom.nextroom.presentation.base +import androidx.fragment.app.setFragmentResultListener import androidx.navigation.fragment.findNavController import com.nextroom.nextroom.presentation.NavGraphDirections import com.nextroom.nextroom.presentation.R @@ -11,22 +12,38 @@ import kotlinx.coroutines.launch abstract class ComposeBaseViewModelFragment : ComposeBaseFragment() { abstract val viewModel: VM + private var pendingErrorAction: NewBaseViewModel.ErrorAction = NewBaseViewModel.ErrorAction.STAY + override fun initObserve() { super.initObserve() + setFragmentResultListener(ERROR_DIALOG_KEY) { _, _ -> + val action = pendingErrorAction + pendingErrorAction = NewBaseViewModel.ErrorAction.STAY + if (action == NewBaseViewModel.ErrorAction.POP_BACK_STACK) { + findNavController().popBackStack() + } + } + viewLifecycleOwner.repeatOnStarted { launch { - viewModel.errorFlow.collect { + viewModel.errorFlow.collect { event -> + pendingErrorAction = event.action NavGraphDirections.moveToNrOneButtonDialog( NROneButtonDialog.NROneButtonArgument( title = getString(R.string.dialog_noti), message = getString(R.string.error_something), btnText = getString(R.string.text_confirm), - errorText = it.message, + errorText = event.throwable.message, + dialogKey = ERROR_DIALOG_KEY, ) ).also { findNavController().safeNavigate(it) } } } } } -} \ No newline at end of file + + companion object { + private const val ERROR_DIALOG_KEY = "compose_base_error_dialog" + } +} diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/base/NewBaseViewModel.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/base/NewBaseViewModel.kt index 4edc7604..9aadfca0 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/base/NewBaseViewModel.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/base/NewBaseViewModel.kt @@ -20,10 +20,10 @@ abstract class NewBaseViewModel : ViewModel() { protected val baseViewModelScope = viewModelScope + exceptionHandler - private val _errorFlow = MutableSharedFlow(extraBufferCapacity = 1) + private val _errorFlow = MutableSharedFlow(extraBufferCapacity = 1) val errorFlow = _errorFlow.asSharedFlow() - fun handleError(throwable: Throwable) { + fun handleError(throwable: Throwable, action: ErrorAction = ErrorAction.STAY) { when (throwable) { is CancellationException -> Unit else -> { @@ -31,8 +31,12 @@ abstract class NewBaseViewModel : ViewModel() { Logger.e("${this::class.simpleName} generated\n${throwable.message}") } - _errorFlow.tryEmit(throwable) + _errorFlow.tryEmit(ErrorEvent(throwable, action)) } } } -} \ No newline at end of file + + enum class ErrorAction { STAY, POP_BACK_STACK } + + data class ErrorEvent(val throwable: Throwable, val action: ErrorAction) +} From 618bc190d5f35ab8313b7cea8cbfd3142c3de144 Mon Sep 17 00:00:00 2001 From: juhwankim-dev Date: Sat, 30 May 2026 16:49:12 +0900 Subject: [PATCH 3/6] =?UTF-8?q?NR-144=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EA=B0=80=20ComposeBaseViewModelFragment?= =?UTF-8?q?=EB=A5=BC=20=EC=83=81=EC=86=8D=EB=B0=9B=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 왜? ComposeBaseViewModelFragment에 에러 처리 관련 로직이 있음. --- .../nextroom/presentation/ui/mypage/MypageFragment.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/mypage/MypageFragment.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/mypage/MypageFragment.kt index c1d5d507..061115a2 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/mypage/MypageFragment.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/mypage/MypageFragment.kt @@ -16,7 +16,7 @@ import androidx.navigation.fragment.findNavController import com.nextroom.nextroom.domain.model.SubscribeStatus import com.nextroom.nextroom.presentation.NavGraphDirections import com.nextroom.nextroom.presentation.R -import com.nextroom.nextroom.presentation.base.ComposeBaseFragment +import com.nextroom.nextroom.presentation.base.ComposeBaseViewModelFragment import com.nextroom.nextroom.presentation.common.NRTwoButtonDialog import com.nextroom.nextroom.presentation.common.compose.NRLoading import com.nextroom.nextroom.presentation.extension.repeatOnStarted @@ -27,11 +27,11 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch @AndroidEntryPoint -class MypageFragment : ComposeBaseFragment() { +class MypageFragment : ComposeBaseViewModelFragment() { override val screenName: String = "mypage" - private val viewModel: MypageViewModel by viewModels() + override val viewModel: MypageViewModel by viewModels() override fun onCreateView( inflater: LayoutInflater, From 50c33008d92dd6716ac12a6b37128d3d6d2ff1d7 Mon Sep 17 00:00:00 2001 From: juhwankim-dev Date: Sat, 30 May 2026 16:50:47 +0900 Subject: [PATCH 4/6] =?UTF-8?q?NR-144=20errorFlow=EB=A5=BC=20channel?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 왜? fragment에서 collect하는 시점이 더 늦더라도 에러 팝업이 뜨는것을 보장하기 위해 channel로 변경함 --- .../nextroom/presentation/base/NewBaseViewModel.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/base/NewBaseViewModel.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/base/NewBaseViewModel.kt index 9aadfca0..7cf69570 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/base/NewBaseViewModel.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/base/NewBaseViewModel.kt @@ -5,8 +5,8 @@ import androidx.lifecycle.viewModelScope import com.nextroom.nextroom.presentation.BuildConfig import com.nextroom.nextroom.presentation.util.Logger import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.plus import kotlin.coroutines.cancellation.CancellationException @@ -20,8 +20,8 @@ abstract class NewBaseViewModel : ViewModel() { protected val baseViewModelScope = viewModelScope + exceptionHandler - private val _errorFlow = MutableSharedFlow(extraBufferCapacity = 1) - val errorFlow = _errorFlow.asSharedFlow() + private val _errorChannel = Channel(Channel.BUFFERED) + val errorFlow = _errorChannel.receiveAsFlow() fun handleError(throwable: Throwable, action: ErrorAction = ErrorAction.STAY) { when (throwable) { @@ -31,7 +31,7 @@ abstract class NewBaseViewModel : ViewModel() { Logger.e("${this::class.simpleName} generated\n${throwable.message}") } - _errorFlow.tryEmit(ErrorEvent(throwable, action)) + _errorChannel.trySend(ErrorEvent(throwable, action)) } } } From 48cd7101183c3db9195684511e35fa5860344c5c Mon Sep 17 00:00:00 2001 From: juhwankim-dev Date: Sat, 30 May 2026 16:53:21 +0900 Subject: [PATCH 5/6] =?UTF-8?q?NR-144=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EC=97=90=EC=84=9C=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=EC=97=90=20=EC=8B=A4=ED=8C=A8=ED=95=9C=20?= =?UTF-8?q?=EA=B2=BD=EC=9A=B0=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 왜? as-is) 로드 실패했을때 사용자가 로딩 화면에 잔류 to-be) 로드 실패했을때 에러 팝업이 뜨고, 확인 버튼을 누르면 이전 화면으로 이동 후자가 더 자연스러운 흐름이라 수정함 --- .../nextroom/presentation/ui/mypage/MypageViewModel.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/mypage/MypageViewModel.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/mypage/MypageViewModel.kt index 627443ee..af0d102d 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/mypage/MypageViewModel.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/mypage/MypageViewModel.kt @@ -52,7 +52,9 @@ class MypageViewModel @Inject constructor( private fun fetchMyInfo() { baseViewModelScope.launch { - adminRepository.getUserSubscribe().onSuccess { mypage -> + runCatching { + adminRepository.getUserSubscribe().getOrThrow + }.onSuccess { mypage -> UiState.Loaded( shopName = mypage.name, status = mypage.status, @@ -61,7 +63,7 @@ class MypageViewModel @Inject constructor( _myInfo.emit(it) } }.onFailure { - _myInfo.emit(UiState.Failure) + handleError(it) } } } From a7bc3fa03f6c215cce4a2b45c4006a78fbd89072 Mon Sep 17 00:00:00 2001 From: juhwankim-dev Date: Sat, 30 May 2026 17:00:10 +0900 Subject: [PATCH 6/6] =?UTF-8?q?NR-144=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 에러 팝업이 뜨게 해두어서 스낵바를 띄우는 코드가 더이상 필요하지 않음. --- .../nextroom/presentation/ui/mypage/MypageFragment.kt | 7 ------- 1 file changed, 7 deletions(-) diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/mypage/MypageFragment.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/mypage/MypageFragment.kt index 061115a2..8acad4ee 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/mypage/MypageFragment.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/mypage/MypageFragment.kt @@ -66,13 +66,6 @@ class MypageFragment : ComposeBaseViewModelFragment() { super.initObserve() viewLifecycleOwner.repeatOnStarted { - launch { - viewModel.uiState.collect { state -> - if (state is MypageViewModel.UiState.Failure) { - snackbar(R.string.error_something) - } - } - } launch { viewModel.uiEvent.collect { event -> when (event) {