Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import com.ninecraft.booket.core.data.api.repository.AuthRepository
import com.ninecraft.booket.feature.home.HomeScreen
import com.orhanobut.logger.Logger
import com.slack.circuit.codegen.annotations.CircuitInject
import com.slack.circuit.retained.rememberRetained
Expand Down Expand Up @@ -51,7 +50,7 @@ class LoginPresenter @AssistedInject constructor(
repository.login(event.accessToken)
.onSuccess { result ->
repository.saveTokens(result.accessToken, result.refreshToken)
navigator.resetRoot(HomeScreen)
navigator.goTo(TermsAgreementScreen)
}.onFailure { exception ->
exception.message?.let { Logger.e(it) }
sideEffect = exception.message?.let {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package com.ninecraft.booket.feature.login

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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
Expand All @@ -17,12 +17,12 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import com.ninecraft.booket.core.designsystem.DevicePreview
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.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
Expand Down Expand Up @@ -63,7 +63,9 @@ internal fun Login(
)

Column(
modifier = modifier.fillMaxSize(),
modifier = modifier
.fillMaxSize()
.background(White),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Expand All @@ -78,8 +80,11 @@ internal fun Login(
},
modifier = Modifier
.fillMaxWidth()
.padding(start = 32.dp, end = 32.dp, bottom = 32.dp)
.height(56.dp)
.padding(
start = ReedTheme.spacing.spacing5,
end = ReedTheme.spacing.spacing5,
bottom = ReedTheme.spacing.spacing8,
)
.align(Alignment.BottomCenter),
colorStyle = ReedButtonColorStyle.KAKAO,
sizeStyle = largeButtonStyle,
Expand Down
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) }
Comment thread
easyhooon marked this conversation as resolved.
Outdated
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>,
Comment thread
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,
Comment thread
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(""))
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Jul 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

URL 파라미터가 빈 문자열로 전달되고 있습니다.

약관 상세 페이지로의 이동 시 실제 URL이 전달되어야 합니다.

-state.eventSink(TermsAgreementScreen.Event.OnTermDetailClick(""))
+state.eventSink(TermsAgreementScreen.Event.OnTermDetailClick(getTermsUrl(index)))

또는 URL을 별도로 관리하는 방식을 고려해보세요.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/TermsAgreementScreen.kt
at line 127, the event OnTermDetailClick is being triggered with an empty string
as the URL parameter. Replace the empty string with the actual URL of the terms
detail page or implement a mechanism to manage and pass the correct URL
dynamically when invoking this event.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아직 url이 정해지지 않아서 임시로 넣었어

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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
Learnt from: seoyoon513
PR: YAPP-Github/Reed-Android#35
File: feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/TermsAgreementScreen.kt:127-127
Timestamp: 2025-07-09T01:14:29.825Z
Learning: In the Reed-Android project's TermsAgreementScreen.kt, the OnTermDetailClick event is intentionally passed an empty string for the URL parameter because the actual URLs for terms detail pages haven't been decided yet. This is a temporary implementation that will be updated once the URLs are finalized.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

},
)
}
}
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 = {},
Comment thread
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 = {},
),
)
}
}
10 changes: 9 additions & 1 deletion feature/login/src/main/res/values/strings.xml
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>
Loading