-
Notifications
You must be signed in to change notification settings - Fork 0
feat: 약관 동의 화면 UI 구현 #35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
8dce649
135e96e
a834fde
7c68058
7a30f4a
41bfdb3
1545cbf
c047329
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| package com.ninecraft.booket.feature.login | ||
|
|
||
| import androidx.compose.runtime.Composable | ||
| import androidx.compose.runtime.getValue | ||
| import androidx.compose.runtime.mutableStateListOf | ||
| import androidx.compose.runtime.mutableStateOf | ||
| import androidx.compose.runtime.setValue | ||
| import com.ninecraft.booket.feature.home.HomeScreen | ||
| import com.slack.circuit.codegen.annotations.CircuitInject | ||
| import com.slack.circuit.retained.rememberRetained | ||
| import com.slack.circuit.runtime.Navigator | ||
| import com.slack.circuit.runtime.presenter.Presenter | ||
| import dagger.assisted.Assisted | ||
| import dagger.assisted.AssistedFactory | ||
| import dagger.assisted.AssistedInject | ||
| import dagger.hilt.android.components.ActivityRetainedComponent | ||
|
|
||
| class TermsAgreementPresenter @AssistedInject constructor( | ||
| @Assisted private val navigator: Navigator, | ||
| ) : Presenter<TermsAgreementScreen.State> { | ||
|
|
||
| @Composable | ||
| override fun present(): TermsAgreementScreen.State { | ||
| var isAllAgreed by rememberRetained { mutableStateOf(false) } | ||
| val agreedTerms = rememberRetained { mutableStateListOf(false, false, false) } | ||
| var isStartButtonEnabled by rememberRetained { mutableStateOf(false) } | ||
|
|
||
| fun handleEvent(event: TermsAgreementScreen.Event) { | ||
| when (event) { | ||
| is TermsAgreementScreen.Event.OnAllTermsAgreedClick -> { | ||
| isAllAgreed = !isAllAgreed | ||
| isStartButtonEnabled = isAllAgreed | ||
|
|
||
| for (i in agreedTerms.indices) { | ||
| agreedTerms[i] = isAllAgreed | ||
| } | ||
| } | ||
| is TermsAgreementScreen.Event.OnTermItemClick -> { | ||
| agreedTerms[event.index] = !agreedTerms[event.index] | ||
|
|
||
| val allIndividualAgreed = agreedTerms.all { it } | ||
|
|
||
| if (allIndividualAgreed) { | ||
| isAllAgreed = true | ||
| isStartButtonEnabled = true | ||
| } else { | ||
| isAllAgreed = false | ||
| isStartButtonEnabled = false | ||
| } | ||
| } | ||
| is TermsAgreementScreen.Event.OnBackClick -> { | ||
| navigator.pop() | ||
| } | ||
| is TermsAgreementScreen.Event.OnTermDetailClick -> { | ||
| // TODO: 웹뷰 화면으로 이동 | ||
| } | ||
| is TermsAgreementScreen.Event.OnStartButtonClick -> { | ||
| navigator.resetRoot(HomeScreen) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return TermsAgreementScreen.State( | ||
| isAllAgreed = isAllAgreed, | ||
| agreedTerms = agreedTerms, | ||
| isStartButtonEnabled = isStartButtonEnabled, | ||
| eventSink = ::handleEvent, | ||
| ) | ||
| } | ||
|
|
||
| @CircuitInject(TermsAgreementScreen::class, ActivityRetainedComponent::class) | ||
| @AssistedFactory | ||
| fun interface Factory { | ||
| fun create(navigator: Navigator): TermsAgreementPresenter | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,204 @@ | ||
| package com.ninecraft.booket.feature.login | ||
|
|
||
| import androidx.compose.foundation.background | ||
| import androidx.compose.foundation.border | ||
| 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.foundation.shape.RoundedCornerShape | ||
| import androidx.compose.material3.Icon | ||
| import androidx.compose.material3.Text | ||
| import androidx.compose.runtime.Composable | ||
| import androidx.compose.ui.Alignment | ||
| import androidx.compose.ui.Modifier | ||
| import androidx.compose.ui.graphics.Color | ||
| import androidx.compose.ui.graphics.vector.ImageVector | ||
| import androidx.compose.ui.res.stringArrayResource | ||
| import androidx.compose.ui.res.stringResource | ||
| import androidx.compose.ui.res.vectorResource | ||
| import androidx.compose.ui.unit.dp | ||
| import com.ninecraft.booket.core.common.extensions.clickableSingle | ||
| import com.ninecraft.booket.core.designsystem.DevicePreview | ||
| import com.ninecraft.booket.core.designsystem.component.appbar.ReedBackTopAppBar | ||
| import com.ninecraft.booket.core.designsystem.component.button.ReedButton | ||
| import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle | ||
| import com.ninecraft.booket.core.designsystem.component.button.largeButtonStyle | ||
| import com.ninecraft.booket.core.designsystem.component.checkbox.SquareCheckBox | ||
| import com.ninecraft.booket.core.designsystem.component.checkbox.TickOnlyCheckBox | ||
| import com.ninecraft.booket.core.designsystem.theme.ReedTheme | ||
| import com.ninecraft.booket.core.designsystem.theme.White | ||
| import com.slack.circuit.codegen.annotations.CircuitInject | ||
| import com.slack.circuit.runtime.CircuitUiEvent | ||
| import com.slack.circuit.runtime.CircuitUiState | ||
| import com.slack.circuit.runtime.screen.Screen | ||
| import dagger.hilt.android.components.ActivityRetainedComponent | ||
| import kotlinx.parcelize.Parcelize | ||
|
|
||
| @Parcelize | ||
| data object TermsAgreementScreen : Screen { | ||
| data class State( | ||
| val isAllAgreed: Boolean, | ||
| val agreedTerms: List<Boolean>, | ||
|
easyhooon marked this conversation as resolved.
Outdated
|
||
| val isStartButtonEnabled: Boolean, | ||
| val eventSink: (Event) -> Unit, | ||
| ) : CircuitUiState | ||
|
|
||
| sealed interface Event : CircuitUiEvent { | ||
| data object OnAllTermsAgreedClick : Event | ||
| data class OnTermItemClick(val index: Int) : Event | ||
| data object OnBackClick : Event | ||
| data class OnTermDetailClick(val url: String) : Event | ||
| data object OnStartButtonClick : Event | ||
| } | ||
| } | ||
|
|
||
| @CircuitInject(TermsAgreementScreen::class, ActivityRetainedComponent::class) | ||
| @Composable | ||
| internal fun TermsAgreement( | ||
| state: TermsAgreementScreen.State, | ||
| modifier: Modifier = Modifier, | ||
| ) { | ||
| Column( | ||
| modifier = modifier | ||
| .fillMaxSize() | ||
| .background(White), | ||
| ) { | ||
| ReedBackTopAppBar( | ||
| onNavigateBack = { | ||
| state.eventSink(TermsAgreementScreen.Event.OnBackClick) | ||
| }, | ||
| ) | ||
| Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) | ||
| Column( | ||
| modifier = Modifier | ||
| .weight(1f) | ||
| .padding(horizontal = ReedTheme.spacing.spacing5), | ||
| ) { | ||
| Text( | ||
| text = stringResource(R.string.terms_agreement_title), | ||
| style = ReedTheme.typography.title2SemiBold, | ||
|
easyhooon marked this conversation as resolved.
Outdated
|
||
| color = ReedTheme.colors.contentPrimary, | ||
| ) | ||
| Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing4)) | ||
| Row( | ||
| modifier = Modifier | ||
| .fillMaxWidth() | ||
| .border( | ||
| width = 1.dp, | ||
| color = ReedTheme.colors.contentBrand, | ||
| shape = RoundedCornerShape(ReedTheme.radius.sm), | ||
| ) | ||
| .padding( | ||
| horizontal = ReedTheme.spacing.spacing4, | ||
| vertical = ReedTheme.spacing.spacing5, | ||
| ), | ||
| verticalAlignment = Alignment.CenterVertically, | ||
| ) { | ||
| SquareCheckBox( | ||
| checked = state.isAllAgreed, | ||
| onCheckedChange = { | ||
| state.eventSink(TermsAgreementScreen.Event.OnAllTermsAgreedClick) | ||
| }, | ||
| ) | ||
| Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing4)) | ||
| Text( | ||
| text = stringResource(R.string.terms_agreement_all), | ||
| style = ReedTheme.typography.headline1SemiBold, | ||
| color = ReedTheme.colors.contentPrimary, | ||
| ) | ||
| } | ||
| Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing4)) | ||
|
|
||
| val termsTitles = stringArrayResource(id = R.array.terms_agreement_items) | ||
|
|
||
| termsTitles.forEachIndexed { index, title -> | ||
| TermItem( | ||
| title = title, | ||
| checked = state.agreedTerms[index], | ||
| onCheckClick = { | ||
| state.eventSink(TermsAgreementScreen.Event.OnTermItemClick(index)) | ||
| }, | ||
| onDetailClick = { | ||
| state.eventSink(TermsAgreementScreen.Event.OnTermDetailClick("")) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. URL 파라미터가 빈 문자열로 전달되고 있습니다. 약관 상세 페이지로의 이동 시 실제 URL이 전달되어야 합니다. -state.eventSink(TermsAgreementScreen.Event.OnTermDetailClick(""))
+state.eventSink(TermsAgreementScreen.Event.OnTermDetailClick(getTermsUrl(index)))또는 URL을 별도로 관리하는 방식을 고려해보세요.
🤖 Prompt for AI Agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아직 url이 정해지지 않아서 임시로 넣었어 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @seoyoon513 이해했습니다! URL이 아직 정해지지 않았다면 임시로 빈 문자열을 사용하는 것이 합리적입니다. 나중에 추적하기 쉽도록 TODO 주석을 추가하는 것을 고려해보세요: onDetailClick = {
// TODO: Replace with actual terms URL once decided
state.eventSink(TermsAgreementScreen.Event.OnTermDetailClick(""))
},URL이 정해지면 해당 부분을 업데이트하시면 됩니다. ✏️ Learnings added
|
||
| }, | ||
| ) | ||
| } | ||
| } | ||
| ReedButton( | ||
| onClick = { | ||
| state.eventSink(TermsAgreementScreen.Event.OnStartButtonClick) | ||
| }, | ||
| modifier = Modifier | ||
| .fillMaxWidth() | ||
| .padding( | ||
| start = ReedTheme.spacing.spacing5, | ||
| end = ReedTheme.spacing.spacing5, | ||
| bottom = ReedTheme.spacing.spacing4, | ||
| ), | ||
| colorStyle = ReedButtonColorStyle.PRIMARY, | ||
| sizeStyle = largeButtonStyle, | ||
| enabled = state.isStartButtonEnabled, | ||
| text = stringResource(R.string.terms_agreement_button_start), | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| @Composable | ||
| private fun TermItem( | ||
| title: String, | ||
| checked: Boolean = false, | ||
| onCheckClick: () -> Unit = {}, | ||
|
easyhooon marked this conversation as resolved.
|
||
| onDetailClick: () -> Unit = {}, | ||
| ) { | ||
| Row( | ||
| modifier = Modifier | ||
| .fillMaxWidth() | ||
| .clickableSingle { | ||
| onDetailClick() | ||
| } | ||
| .padding( | ||
| start = ReedTheme.spacing.spacing5, | ||
| end = ReedTheme.spacing.spacing3, | ||
| top = ReedTheme.spacing.spacing2, | ||
| bottom = ReedTheme.spacing.spacing2, | ||
| ), | ||
| verticalAlignment = Alignment.CenterVertically, | ||
| ) { | ||
| TickOnlyCheckBox( | ||
| checked = checked, | ||
| onCheckedChange = { onCheckClick() }, | ||
| ) | ||
| Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing1)) | ||
| Text( | ||
| modifier = Modifier.weight(1f), | ||
| text = title, | ||
| style = ReedTheme.typography.body1Medium, | ||
| color = ReedTheme.colors.contentPrimary, | ||
| ) | ||
| Icon( | ||
| imageVector = ImageVector.vectorResource(id = com.ninecraft.booket.core.designsystem.R.drawable.ic_chevron_right), | ||
| contentDescription = "Navigation Icon", | ||
| tint = Color.Unspecified, | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| @DevicePreview | ||
| @Composable | ||
| private fun TermsAgreementPreview() { | ||
| ReedTheme { | ||
| TermsAgreement( | ||
| state = TermsAgreementScreen.State( | ||
| isAllAgreed = false, | ||
| agreedTerms = listOf(false, false, false), | ||
| isStartButtonEnabled = false, | ||
| eventSink = {}, | ||
| ), | ||
| ) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,12 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <resources> | ||
| <string name="kakao_login">카카오 로그인</string> | ||
| <string name="kakao_login">카카오로 시작하기</string> | ||
| <string name="terms_agreement_title">약관 동의 후\n독서 기록을 남겨보세요</string> | ||
| <string name="terms_agreement_all">약관 전체 동의</string> | ||
| <string name="terms_agreement_button_start">시작하기</string> | ||
| <string-array name="terms_agreement_items"> | ||
| <item>(필수)서비스 이용약관</item> | ||
| <item>(필수)개인정보처리방침</item> | ||
| <item>(필수)만 14세 이상입니다</item> | ||
| </string-array> | ||
| </resources> |
Uh oh!
There was an error while loading. Please reload this page.