diff --git a/feature/auth/src/main/java/com/project200/undabang/auth/register/PermissionFragment.kt b/feature/auth/src/main/java/com/project200/undabang/auth/register/PermissionFragment.kt index e5afb229..6a778990 100644 --- a/feature/auth/src/main/java/com/project200/undabang/auth/register/PermissionFragment.kt +++ b/feature/auth/src/main/java/com/project200/undabang/auth/register/PermissionFragment.kt @@ -4,17 +4,20 @@ import android.Manifest import android.content.pm.PackageManager import android.os.Build import android.os.Bundle +import android.view.LayoutInflater import android.view.View +import android.view.ViewGroup import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.ui.platform.ComposeView import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController -import com.project200.presentation.base.BindingFragment // BaseFragment 경로 확인 및 수정 필요 +import com.project200.presentation.compose.applyAppTheme import com.project200.undabang.feature.auth.R -import com.project200.undabang.feature.auth.databinding.FragmentPermissionBinding import timber.log.Timber -class PermissionFragment : BindingFragment(R.layout.fragment_permission) { +class PermissionFragment : Fragment() { private lateinit var requestMultiplePermissionsLauncher: ActivityResultLauncher> private lateinit var requestNotificationPermissionLauncher: ActivityResultLauncher @@ -51,9 +54,22 @@ class PermissionFragment : BindingFragment(R.layout.f } } - override fun getViewBinding(view: View): FragmentPermissionBinding { - return FragmentPermissionBinding.bind(view) - } + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View = + ComposeView(requireContext()).apply { + applyAppTheme { + PermissionScreen( + onNextClick = { + if (isAdded && findNavController().currentDestination?.id == R.id.permissionFragment) { + findNavController().navigate(R.id.action_permissionFragment_to_termsFragment) + } + }, + ) + } + } override fun onViewCreated( view: View, @@ -64,14 +80,6 @@ class PermissionFragment : BindingFragment(R.layout.f requestNeededPermissions() } - override fun setupViews() { - binding.permissionNextBtn.setOnClickListener { - if (isAdded && findNavController().currentDestination?.id == R.id.permissionFragment) { - findNavController().navigate(R.id.action_permissionFragment_to_termsFragment) - } - } - } - private fun requestNeededPermissions() { requiredPermissions.clear() diff --git a/feature/auth/src/main/java/com/project200/undabang/auth/register/PermissionScreen.kt b/feature/auth/src/main/java/com/project200/undabang/auth/register/PermissionScreen.kt new file mode 100644 index 00000000..4195781e --- /dev/null +++ b/feature/auth/src/main/java/com/project200/undabang/auth/register/PermissionScreen.kt @@ -0,0 +1,125 @@ +package com.project200.undabang.auth.register + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +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.width +import androidx.compose.material3.MaterialTheme +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.project200.presentation.compose.components.button.PrimaryButton +import com.project200.presentation.compose.theme.AppTheme +import com.project200.presentation.compose.theme.ColorGray200 +import com.project200.presentation.compose.theme.ColorWhite300 +import com.project200.presentation.compose.theme.contentBold +import com.project200.presentation.compose.theme.header +import com.project200.presentation.compose.theme.subtext12 +import com.project200.undabang.feature.auth.R +import com.project200.undabang.presentation.R as PresentationR + +@Composable +fun PermissionScreen( + onNextClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = + modifier + .fillMaxSize() + .background(ColorWhite300) + .padding(horizontal = 20.dp), + ) { + Spacer(Modifier.height(114.dp)) + + Text( + text = stringResource(R.string.permission_title), + style = MaterialTheme.typography.header, + ) + + Spacer(Modifier.height(77.dp)) + + PermissionRow( + iconRes = R.drawable.ic_permission_location, + title = stringResource(R.string.location_title), + desc = stringResource(R.string.location_desc), + ) + + Spacer(Modifier.height(24.dp)) + + PermissionRow( + iconRes = R.drawable.ic_permission_notify, + title = stringResource(R.string.notify_title), + desc = stringResource(R.string.notify_desc), + ) + + Spacer(Modifier.height(24.dp)) + + PermissionRow( + iconRes = R.drawable.ic_permission_gallery, + title = stringResource(R.string.gallery_title), + desc = stringResource(R.string.gallery_desc), + ) + + Spacer(Modifier.weight(1f)) + + PrimaryButton( + text = stringResource(PresentationR.string.confirm), + onClick = onNextClick, + modifier = Modifier.fillMaxWidth(), + ) + + Spacer(Modifier.height(32.dp)) + } +} + +@Composable +private fun PermissionRow( + @DrawableRes iconRes: Int, + title: String, + desc: String, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + painter = painterResource(iconRes), + contentDescription = null, + ) + Spacer(Modifier.width(24.dp)) + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text( + text = title, + style = MaterialTheme.typography.contentBold, + ) + Text( + text = desc, + style = MaterialTheme.typography.subtext12, + color = ColorGray200, + ) + } + } +} + +@Preview(showBackground = true, heightDp = 720) +@Composable +private fun PermissionScreenPreview() { + AppTheme { + PermissionScreen(onNextClick = {}) + } +} diff --git a/feature/auth/src/main/java/com/project200/undabang/auth/register/TermsViewModel.kt b/feature/auth/src/main/java/com/project200/undabang/auth/register/TermsViewModel.kt index 20bcbb55..5a37722d 100644 --- a/feature/auth/src/main/java/com/project200/undabang/auth/register/TermsViewModel.kt +++ b/feature/auth/src/main/java/com/project200/undabang/auth/register/TermsViewModel.kt @@ -24,7 +24,7 @@ class TermsViewModel val isAllRequiredChecked: StateFlow = combine(_serviceChecked, _privacyChecked) { service, privacy -> service && privacy - }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), false) + }.stateIn(viewModelScope, SharingStarted.Eagerly, false) fun toggleService() { _serviceChecked.value = !_serviceChecked.value diff --git a/feature/auth/src/main/res/layout/fragment_permission.xml b/feature/auth/src/main/res/layout/fragment_permission.xml deleted file mode 100644 index c78a65ef..00000000 --- a/feature/auth/src/main/res/layout/fragment_permission.xml +++ /dev/null @@ -1,126 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/feature/auth/src/main/res/navigation/onboardong_nav_graph.xml b/feature/auth/src/main/res/navigation/onboardong_nav_graph.xml index b5a3c66a..09027ef8 100644 --- a/feature/auth/src/main/res/navigation/onboardong_nav_graph.xml +++ b/feature/auth/src/main/res/navigation/onboardong_nav_graph.xml @@ -8,8 +8,7 @@ + android:label="fragment_permission" > diff --git a/feature/auth/src/test/java/com/project200/undabang/auth/TermsViewModelTest.kt b/feature/auth/src/test/java/com/project200/undabang/auth/TermsViewModelTest.kt index d5ef54b8..1e76e6ed 100644 --- a/feature/auth/src/test/java/com/project200/undabang/auth/TermsViewModelTest.kt +++ b/feature/auth/src/test/java/com/project200/undabang/auth/TermsViewModelTest.kt @@ -1,128 +1,121 @@ package com.project200.undabang.auth -import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.google.common.truth.Truth.assertThat import com.project200.undabang.auth.register.TermsViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After import org.junit.Before -import org.junit.Rule import org.junit.Test +@ExperimentalCoroutinesApi class TermsViewModelTest { - @get:Rule - val instantTaskExecutorRule = InstantTaskExecutorRule() - + private val testDispatcher = StandardTestDispatcher() private lateinit var viewModel: TermsViewModel @Before fun setUp() { + Dispatchers.setMain(testDispatcher) viewModel = TermsViewModel() } + @After + fun tearDown() { + Dispatchers.resetMain() + } + @Test fun `초기 상태에서 서비스 약관은 체크되지 않음`() { - // Then assertThat(viewModel.serviceChecked.value).isFalse() } @Test fun `초기 상태에서 개인정보 약관은 체크되지 않음`() { - // Then assertThat(viewModel.privacyChecked.value).isFalse() } @Test fun `초기 상태에서 필수 약관 전체 동의는 false`() { - // Then assertThat(viewModel.isAllRequiredChecked.value).isFalse() } @Test fun `toggleService - 서비스 약관 토글 시 상태가 변경된다`() { - // Given assertThat(viewModel.serviceChecked.value).isFalse() - // When viewModel.toggleService() - // Then assertThat(viewModel.serviceChecked.value).isTrue() } @Test fun `toggleService - 두 번 토글 시 원래 상태로 돌아온다`() { - // When viewModel.toggleService() viewModel.toggleService() - // Then assertThat(viewModel.serviceChecked.value).isFalse() } @Test fun `togglePrivacy - 개인정보 약관 토글 시 상태가 변경된다`() { - // Given assertThat(viewModel.privacyChecked.value).isFalse() - // When viewModel.togglePrivacy() - // Then assertThat(viewModel.privacyChecked.value).isTrue() } @Test fun `togglePrivacy - 두 번 토글 시 원래 상태로 돌아온다`() { - // When viewModel.togglePrivacy() viewModel.togglePrivacy() - // Then assertThat(viewModel.privacyChecked.value).isFalse() } @Test - fun `isAllRequiredChecked - 서비스 약관만 체크하면 false`() { - // When - viewModel.toggleService() + fun `isAllRequiredChecked - 서비스 약관만 체크하면 false`() = + runTest { + viewModel.toggleService() + testDispatcher.scheduler.advanceUntilIdle() - // Then - assertThat(viewModel.isAllRequiredChecked.value).isFalse() - } + assertThat(viewModel.isAllRequiredChecked.value).isFalse() + } @Test - fun `isAllRequiredChecked - 개인정보 약관만 체크하면 false`() { - // When - viewModel.togglePrivacy() + fun `isAllRequiredChecked - 개인정보 약관만 체크하면 false`() = + runTest { + viewModel.togglePrivacy() + testDispatcher.scheduler.advanceUntilIdle() - // Then - assertThat(viewModel.isAllRequiredChecked.value).isFalse() - } + assertThat(viewModel.isAllRequiredChecked.value).isFalse() + } @Test - fun `isAllRequiredChecked - 모든 필수 약관 체크 시 true`() { - // Given - viewModel.isAllRequiredChecked.observeForever {} - - // When - viewModel.toggleService() - viewModel.togglePrivacy() + fun `isAllRequiredChecked - 모든 필수 약관 체크 시 true`() = + runTest { + viewModel.toggleService() + viewModel.togglePrivacy() + testDispatcher.scheduler.advanceUntilIdle() - // Then - assertThat(viewModel.isAllRequiredChecked.value).isTrue() - } + assertThat(viewModel.isAllRequiredChecked.value).isTrue() + } @Test - fun `isAllRequiredChecked - 모든 약관 체크 후 하나 해제하면 false`() { - // Given - viewModel.isAllRequiredChecked.observeForever {} - viewModel.toggleService() - viewModel.togglePrivacy() - assertThat(viewModel.isAllRequiredChecked.value).isTrue() - - // When - viewModel.toggleService() - - // Then - assertThat(viewModel.isAllRequiredChecked.value).isFalse() - } + fun `isAllRequiredChecked - 모든 약관 체크 후 하나 해제하면 false`() = + runTest { + viewModel.toggleService() + viewModel.togglePrivacy() + testDispatcher.scheduler.advanceUntilIdle() + assertThat(viewModel.isAllRequiredChecked.value).isTrue() + + viewModel.toggleService() + testDispatcher.scheduler.advanceUntilIdle() + + assertThat(viewModel.isAllRequiredChecked.value).isFalse() + } }