Skip to content

Commit b739342

Browse files
약관동의 화면 구현 (#92)
* refactor: PrezelTopAppBar 컴포넌트에 windowInsets 파라미터 추가 * refactor: PrezelCheckbox를 PrezelTouchArea 기반으로 변경 * feat: 로그인 모듈 내 BuildConfig 활성화 및 약관 URL 필드 추가 * feat: 약관 동의 화면 구현 * refactor: `TermsScreen`컴포저블 함수를 논리적 단위로 분리하여 가독성과 유지보수성 개선 * style: TermsScreen 내부 함수 네이밍 변경 * feat: LoginViewModel 내 디버그 모드 전용 로직 추가 * feat: LoginViewModel 내 디버그 모드 전용 로직 추가 * refactor: PrezelCheckbox 접근성 시맨틱 속성 추가 `PrezelCheckbox` 컴포넌트에 접근성 지원을 위한 시맨틱 정보를 추가하였습니다. * `modifier`에 `Role.Checkbox`를 지정하여 컴포넌트의 역할을 명시했습니다. * `ToggleableState`를 통해 현재 체크 상태(`checked`)를 시맨틱 트리에 반영했습니다. * fix: LoginViewModel 내 디버그 모드 로그인 로딩 상태 처리 수정 * refactor: 디버그 환경에서 로그인 시 로딩 상태를 해제하도록 수정 MVP 개발용 디버그 로직에서 약관 화면(`NavigateToTerms`)으로 이동하기 전, `isLoading` 상태를 `false`로 업데이트하여 로딩 인디케이터가 남지 않도록 개선했습니다. * refactor: `WebView`의 리소스 누수 방지 및 초기화 로직 최적화 * refactor: TermsUiState 체크 상태 계산 로직 수정 * style: TermsScreen 내 UI Intent 네이밍 정리 및 코드 포맷팅 수정 * refactor: `TermsUiIntent` 네이밍 변경 `TermsUiIntent`의 각 항목에서 불필요한 `On` 접두사를 제거하여 네이밍을 간결하게 수정했습니다. - `OnToggleAll` -> `ToggleAll` - `OnToggleTermsOfService` -> `ToggleTermsOfService` - `OnTogglePrivacyPolicy` -> `TogglePrivacyPolicy` - `OnToggleMarketingConsent` -> `ToggleMarketingConsent` - `OnClickContinue` -> `ClickContinue` * refactor: TermsViewModel 코드 포맷팅 및 구조 정리 - `TermsViewModel` 생성자 및 내부 메서드의 들여쓰기(Indentation)를 수정하여 가독성을 개선했습니다. - 변경된 `TermsUiIntent` 명칭에 맞춰 `onIntent` 내 `when` 분기 로직을 업데이트했습니다. * refactor: TermsScreen 내 Intent 호출부 업데이트 - `TermsScreen` 컴포저블에서 `ViewModel`로 Intent를 전달하는 코드를 신규 네이밍 규칙에 맞게 수정했습니다. * style: TermsScreen 내 UI Intent 네이밍 정리 및 코드 포맷팅 수정 * refactor: `TermsUiIntent` 네이밍 변경 `TermsUiIntent`의 각 항목에서 불필요한 `On` 접두사를 제거하여 네이밍을 간결하게 수정했습니다. - `OnToggleAll` -> `ToggleAll` - `OnToggleTermsOfService` -> `ToggleTermsOfService` - `OnTogglePrivacyPolicy` -> `TogglePrivacyPolicy` - `OnToggleMarketingConsent` -> `ToggleMarketingConsent` - `OnClickContinue` -> `ClickContinue` * refactor: TermsViewModel 코드 포맷팅 및 구조 정리 - `TermsViewModel` 생성자 및 내부 메서드의 들여쓰기(Indentation)를 수정하여 가독성을 개선했습니다. - 변경된 `TermsUiIntent` 명칭에 맞춰 `onIntent` 내 `when` 분기 로직을 업데이트했습니다. * refactor: TermsScreen 내 Intent 호출부 업데이트 - `TermsScreen` 컴포저블에서 `ViewModel`로 Intent를 전달하는 코드를 신규 네이밍 규칙에 맞게 수정했습니다. --------- Co-authored-by: Ham BeomJoon <hbj0802@naver.com>
1 parent 963efaf commit b739342

21 files changed

Lines changed: 705 additions & 106 deletions

File tree

Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelAccordion.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import androidx.compose.foundation.clickable
1212
import androidx.compose.foundation.layout.Arrangement
1313
import androidx.compose.foundation.layout.Box
1414
import androidx.compose.foundation.layout.Column
15+
import androidx.compose.foundation.layout.PaddingValues
1516
import androidx.compose.foundation.layout.fillMaxWidth
1617
import androidx.compose.foundation.layout.padding
1718
import androidx.compose.foundation.layout.size
@@ -176,6 +177,7 @@ private fun PrezelAccordionPreview() {
176177
modifier = Modifier.drawDashBorder(),
177178
size = CheckboxSize.REGULAR,
178179
onCheckedChange = { state = !state },
180+
extraTouchPadding = PaddingValues(),
179181
)
180182
},
181183
trailingContent = {

Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelCheckbox.kt

Lines changed: 41 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,27 @@
11
package com.team.prezel.core.designsystem.component
22

3-
import androidx.compose.foundation.layout.Box
4-
import androidx.compose.foundation.layout.padding
3+
import androidx.compose.foundation.layout.PaddingValues
54
import androidx.compose.foundation.layout.size
6-
import androidx.compose.foundation.selection.toggleable
75
import androidx.compose.material3.Icon
86
import androidx.compose.runtime.Composable
97
import androidx.compose.runtime.getValue
108
import androidx.compose.runtime.mutableStateOf
119
import androidx.compose.runtime.remember
1210
import androidx.compose.runtime.setValue
13-
import androidx.compose.ui.Alignment
1411
import androidx.compose.ui.Modifier
12+
import androidx.compose.ui.graphics.Color
13+
import androidx.compose.ui.graphics.painter.Painter
1514
import androidx.compose.ui.res.painterResource
1615
import androidx.compose.ui.res.stringResource
1716
import androidx.compose.ui.semantics.Role
17+
import androidx.compose.ui.semantics.role
18+
import androidx.compose.ui.semantics.semantics
19+
import androidx.compose.ui.semantics.toggleableState
20+
import androidx.compose.ui.state.ToggleableState
21+
import androidx.compose.ui.unit.Dp
1822
import androidx.compose.ui.unit.dp
1923
import com.team.prezel.core.designsystem.R
24+
import com.team.prezel.core.designsystem.component.base.PrezelTouchArea
2025
import com.team.prezel.core.designsystem.icon.PrezelIcons
2126
import com.team.prezel.core.designsystem.preview.BasicPreview
2227
import com.team.prezel.core.designsystem.preview.PreviewSection
@@ -34,48 +39,47 @@ fun PrezelCheckbox(
3439
checked: Boolean,
3540
modifier: Modifier = Modifier,
3641
size: CheckboxSize = CheckboxSize.REGULAR,
42+
extraTouchPadding: PaddingValues = PaddingValues(all = PrezelTheme.spacing.V8),
3743
onCheckedChange: (Boolean) -> Unit,
3844
) {
39-
val checkboxSize = when (size) {
40-
CheckboxSize.REGULAR -> 24.dp
41-
CheckboxSize.LARGE -> 32.dp
42-
}
43-
44-
val iconRes =
45-
if (checked) {
46-
PrezelIcons.CheckCircleFilled
47-
} else {
48-
PrezelIcons.CheckCircleOutlined
49-
}
50-
51-
val iconColor =
52-
if (checked) {
53-
PrezelTheme.colors.feedbackGoodRegular
54-
} else {
55-
PrezelTheme.colors.iconDisabled
56-
}
57-
58-
Box(
59-
modifier = modifier
60-
.padding(all = PrezelTheme.spacing.V8)
61-
.toggleable(
62-
value = checked,
63-
interactionSource = null,
64-
indication = null,
65-
role = Role.Checkbox,
66-
onValueChange = onCheckedChange,
67-
),
68-
contentAlignment = Alignment.Center,
45+
PrezelTouchArea(
46+
modifier = modifier.semantics {
47+
role = Role.Checkbox
48+
toggleableState = ToggleableState(checked)
49+
},
50+
onClick = { onCheckedChange(!checked) },
51+
isUseRipple = false,
52+
extraTouchPadding = extraTouchPadding,
6953
) {
7054
Icon(
71-
painter = painterResource(id = iconRes),
55+
painter = checkboxIconRes(checked = checked),
7256
contentDescription = stringResource(R.string.core_designsystem_checkbox_desc),
73-
modifier = Modifier.size(checkboxSize),
74-
tint = iconColor,
57+
modifier = Modifier.size(size = checkboxSize(size = size)),
58+
tint = checkboxIconTintColor(checked = checked),
7559
)
7660
}
7761
}
7862

63+
private fun checkboxSize(size: CheckboxSize): Dp =
64+
when (size) {
65+
CheckboxSize.REGULAR -> 24.dp
66+
CheckboxSize.LARGE -> 32.dp
67+
}
68+
69+
@Composable
70+
private fun checkboxIconRes(checked: Boolean): Painter {
71+
val resId = if (checked) PrezelIcons.CheckCircleFilled else PrezelIcons.CheckCircleOutlined
72+
return painterResource(id = resId)
73+
}
74+
75+
@Composable
76+
private fun checkboxIconTintColor(checked: Boolean): Color =
77+
if (checked) {
78+
PrezelTheme.colors.feedbackGoodRegular
79+
} else {
80+
PrezelTheme.colors.iconDisabled
81+
}
82+
7983
@BasicPreview
8084
@Composable
8185
private fun PrezelCheckboxPreview() {

Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/TopAppBar.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.team.prezel.core.designsystem.component
22

33
import androidx.compose.foundation.layout.RowScope
4+
import androidx.compose.foundation.layout.WindowInsets
45
import androidx.compose.material3.ExperimentalMaterial3Api
56
import androidx.compose.material3.Icon
67
import androidx.compose.material3.IconButton
@@ -27,6 +28,7 @@ fun PrezelTopAppBar(
2728
leadingIcon: @Composable () -> Unit = {},
2829
trailingIcons: @Composable RowScope.() -> Unit = {},
2930
scrollBehavior: TopAppBarScrollBehavior? = null,
31+
windowInsets: WindowInsets = WindowInsets(),
3032
) {
3133
TopAppBar(
3234
title = {
@@ -38,6 +40,7 @@ fun PrezelTopAppBar(
3840
actions = trailingIcons,
3941
colors = prezelTopAppBarColors(),
4042
scrollBehavior = scrollBehavior,
43+
windowInsets = windowInsets,
4144
modifier = modifier.testTag("PrezelTopAppBar"),
4245
)
4346
}

Prezel/feature/login/impl/build.gradle.kts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
1+
import com.team.prezel.buildlogic.convention.external.localProperty
2+
13
plugins {
24
alias(libs.plugins.prezel.android.feature.impl)
35
}
46

57
android {
68
namespace = "com.team.prezel.feature.login.impl"
9+
10+
buildFeatures {
11+
buildConfig = true
12+
}
13+
14+
defaultConfig {
15+
buildConfigField("String", "PRIVACY_POLICY_URL", "\"${localProperty("privacy.policy.url").get()}\"")
16+
buildConfigField("String", "TERMS_OF_SERVICE_URL", "\"${localProperty("terms.of.service.url").get()}\"")
17+
}
718
}
819

920
dependencies {

Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/LoginScreen.kt renamed to Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/LoginScreen.kt

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.team.prezel.feature.login.impl
1+
package com.team.prezel.feature.login.impl.landing
22

33
import androidx.compose.animation.AnimatedVisibility
44
import androidx.compose.animation.AnimatedVisibilityScope
@@ -25,8 +25,10 @@ import androidx.compose.ui.graphics.Color
2525
import androidx.compose.ui.platform.LocalContext
2626
import androidx.compose.ui.platform.LocalResources
2727
import androidx.compose.ui.res.painterResource
28+
import androidx.compose.ui.res.stringResource
2829
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
2930
import androidx.lifecycle.compose.collectAsStateWithLifecycle
31+
import androidx.navigation3.ui.LocalNavAnimatedContentScope
3032
import com.team.prezel.core.auth.AuthManager
3133
import com.team.prezel.core.auth.model.AuthProvider
3234
import com.team.prezel.core.designsystem.component.actions.area.PrezelButtonArea
@@ -40,58 +42,54 @@ import com.team.prezel.core.designsystem.preview.BasicPreview
4042
import com.team.prezel.core.designsystem.theme.PrezelTheme
4143
import com.team.prezel.core.ui.LocalSnackbarHostState
4244
import com.team.prezel.feature.login.api.AUTH_LOGO_SHARED_ELEMENT_KEY
43-
import com.team.prezel.feature.login.impl.model.LoginUiMessage
44-
import com.team.prezel.feature.login.impl.viewModel.LoginUiEffect
45-
import com.team.prezel.feature.login.impl.viewModel.LoginUiIntent
46-
import com.team.prezel.feature.login.impl.viewModel.LoginUiState
47-
import com.team.prezel.feature.login.impl.viewModel.LoginViewModel
45+
import com.team.prezel.feature.login.impl.R
46+
import com.team.prezel.feature.login.impl.landing.contract.LoginUiEffect
47+
import com.team.prezel.feature.login.impl.landing.contract.LoginUiIntent
48+
import com.team.prezel.feature.login.impl.landing.contract.LoginUiState
49+
import com.team.prezel.feature.login.impl.landing.model.LoginUiMessage
4850
import com.team.prezel.core.designsystem.R as DSR
4951

5052
private const val AUTH_SHARED_ELEMENT_TRANSITION_DURATION = 300
5153
private const val AUTH_SHARED_ELEMENT_TRANSITION_DELAY = 400
5254

5355
@Composable
5456
internal fun SharedTransitionScope.LoginScreen(
55-
animatedVisibilityScope: AnimatedVisibilityScope,
5657
authManager: AuthManager,
57-
navigateToHome: () -> Unit,
58+
navigateToTerms: () -> Unit,
5859
modifier: Modifier = Modifier,
5960
viewModel: LoginViewModel = hiltViewModel(),
6061
) {
6162
val context = LocalContext.current
6263
val resources = LocalResources.current
63-
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
6464
val snackbarHostState = LocalSnackbarHostState.current
65+
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
6566

6667
LaunchedEffect(Unit) {
6768
viewModel.uiEffect.collect { effect ->
6869
when (effect) {
69-
LoginUiEffect.NavigateToHome -> navigateToHome()
70+
is LoginUiEffect.LaunchLogin -> {
71+
authManager.login(context = context, provider = effect.provider).also { result ->
72+
viewModel.onIntent(LoginUiIntent.OnLoginResult(result = result))
73+
}
74+
}
75+
76+
LoginUiEffect.NavigateToTerms -> navigateToTerms()
7077

7178
is LoginUiEffect.ShowMessage -> {
7279
val resId = when (effect.message) {
7380
LoginUiMessage.LoginCancelled -> R.string.feature_login_impl_kakao_cancelled
7481
LoginUiMessage.LoginFailedRateLimited -> R.string.feature_login_impl_kakao_rate_limited
7582
LoginUiMessage.LoginFailedUnknown -> R.string.feature_login_impl_kakao_failure
7683
}
77-
snackbarHostState.showPrezelSnackbar(
78-
message = resources.getString(resId),
79-
actionLabel = resources.getString(R.string.feature_login_impl_snackbar_confirm),
80-
)
81-
}
82-
83-
is LoginUiEffect.LaunchLogin -> {
84-
authManager.login(context = context, provider = effect.provider).also { result ->
85-
viewModel.onIntent(LoginUiIntent.OnLoginResult(result = result))
86-
}
84+
snackbarHostState.showPrezelSnackbar(message = resources.getString(resId))
8785
}
8886
}
8987
}
9088
}
9189

9290
LoginScreen(
9391
uiState = uiState,
94-
animatedVisibilityScope = animatedVisibilityScope,
92+
animatedVisibilityScope = LocalNavAnimatedContentScope.current,
9593
onLogin = { viewModel.onIntent(LoginUiIntent.OnClickLogin(provider = AuthProvider.KAKAO)) },
9694
modifier = modifier,
9795
)
@@ -153,6 +151,7 @@ private fun LoginFooter(
153151
modifier: Modifier = Modifier,
154152
) {
155153
var isButtonVisible by remember { mutableStateOf(false) }
154+
val startWithKakaoLabel = stringResource(R.string.feature_login_impl_start_with_kakao)
156155
val kakaoButtonConfig = PrezelButtonDefaults.getDefault(
157156
isIconOnly = false,
158157
type = ButtonType.FILLED,
@@ -182,7 +181,7 @@ private fun LoginFooter(
182181
PrezelButtonArea {
183182
CustomButton(
184183
iconResId = PrezelIcons.Kakao,
185-
label = "카카오로 시작하기",
184+
label = startWithKakaoLabel,
186185
enabled = enabled,
187186
onClick = onLogin,
188187
config = kakaoButtonConfig,

Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginViewModel.kt renamed to Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/LoginViewModel.kt

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1-
package com.team.prezel.feature.login.impl.viewModel
1+
package com.team.prezel.feature.login.impl.landing
22

33
import androidx.lifecycle.ViewModel
44
import androidx.lifecycle.viewModelScope
5+
import com.team.prezel.core.auth.model.AuthProvider
56
import com.team.prezel.core.auth.model.AuthResult
6-
import com.team.prezel.feature.login.impl.model.LoginUiMessage
7+
import com.team.prezel.feature.login.impl.BuildConfig
8+
import com.team.prezel.feature.login.impl.landing.contract.LoginUiEffect
9+
import com.team.prezel.feature.login.impl.landing.contract.LoginUiIntent
10+
import com.team.prezel.feature.login.impl.landing.contract.LoginUiState
11+
import com.team.prezel.feature.login.impl.landing.model.LoginUiMessage
712
import dagger.hilt.android.lifecycle.HiltViewModel
813
import kotlinx.coroutines.channels.Channel
914
import kotlinx.coroutines.flow.Flow
@@ -15,20 +20,20 @@ import kotlinx.coroutines.launch
1520
import javax.inject.Inject
1621

1722
@HiltViewModel
18-
class LoginViewModel
23+
internal class LoginViewModel
1924
@Inject
2025
constructor() : ViewModel() {
2126
private val _uiState = MutableStateFlow(LoginUiState())
2227
val uiState: StateFlow<LoginUiState> = _uiState
23-
val currentState: LoginUiState
28+
private val currentState: LoginUiState
2429
get() = uiState.value
2530

2631
private val _uiEffect = Channel<LoginUiEffect>()
2732
val uiEffect: Flow<LoginUiEffect> = _uiEffect.receiveAsFlow()
2833

2934
fun onIntent(intent: LoginUiIntent) {
3035
when (intent) {
31-
is LoginUiIntent.OnClickLogin -> handleClickLogin()
36+
is LoginUiIntent.OnClickLogin -> handleClickLogin(provider = intent.provider)
3237
is LoginUiIntent.OnLoginResult -> handleLoginResult(result = intent.result)
3338
}
3439
}
@@ -37,14 +42,19 @@ class LoginViewModel
3742
_uiState.update(reducer)
3843
}
3944

40-
private fun handleClickLogin() {
45+
private fun handleClickLogin(provider: AuthProvider) {
4146
if (currentState.isLoading) return
4247

4348
viewModelScope.launch {
4449
update { copy(isLoading = true) }
4550

46-
// _uiEffect.send(LoginUiEffect.LaunchLogin(provider = provider))
47-
_uiEffect.send(LoginUiEffect.NavigateToHome)
51+
// todo: MVP 개발 완료 후 해당 조건 제거
52+
if (BuildConfig.DEBUG) {
53+
update { copy(isLoading = false) }
54+
_uiEffect.send(LoginUiEffect.NavigateToTerms)
55+
} else {
56+
_uiEffect.send(LoginUiEffect.LaunchLogin(provider = provider))
57+
}
4858
}
4959
}
5060

@@ -53,22 +63,14 @@ class LoginViewModel
5363
update { copy(isLoading = false) }
5464

5565
when (result) {
56-
AuthResult.Success -> {
57-
_uiEffect.send(LoginUiEffect.NavigateToHome)
58-
}
59-
60-
AuthResult.Cancelled -> {
61-
_uiEffect.send(LoginUiEffect.ShowMessage(LoginUiMessage.LoginCancelled))
62-
}
63-
64-
is AuthResult.Failure -> {
65-
_uiEffect.send(LoginUiEffect.ShowMessage(result.toLoginUiMessage()))
66-
}
66+
AuthResult.Success -> _uiEffect.send(LoginUiEffect.NavigateToTerms)
67+
AuthResult.Cancelled -> _uiEffect.send(LoginUiEffect.ShowMessage(LoginUiMessage.LoginCancelled))
68+
is AuthResult.Failure -> _uiEffect.send(LoginUiEffect.ShowMessage(result.toUiMessage()))
6769
}
6870
}
6971
}
7072

71-
private fun AuthResult.Failure.toLoginUiMessage(): LoginUiMessage =
73+
private fun AuthResult.Failure.toUiMessage(): LoginUiMessage =
7274
when (this) {
7375
AuthResult.Failure.RateLimited -> LoginUiMessage.LoginFailedRateLimited
7476
AuthResult.Failure.Unknown -> LoginUiMessage.LoginFailedUnknown

Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginUiEffect.kt renamed to Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/contract/LoginUiEffect.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
package com.team.prezel.feature.login.impl.viewModel
1+
package com.team.prezel.feature.login.impl.landing.contract
22

33
import com.team.prezel.core.auth.model.AuthProvider
4-
import com.team.prezel.feature.login.impl.model.LoginUiMessage
4+
import com.team.prezel.feature.login.impl.landing.model.LoginUiMessage
55

6-
sealed interface LoginUiEffect {
6+
internal sealed interface LoginUiEffect {
77
data class LaunchLogin(
88
val provider: AuthProvider,
99
) : LoginUiEffect
1010

11-
data object NavigateToHome : LoginUiEffect
11+
data object NavigateToTerms : LoginUiEffect
1212

1313
data class ShowMessage(
1414
val message: LoginUiMessage,

0 commit comments

Comments
 (0)