Skip to content

Conversation

@stopstone
Copy link
Contributor

@stopstone stopstone commented Sep 26, 2025

작업 내용

  • 로그인 성공 후 바텀시트를 열어 개인정보를 동의합니다.
  • 체크박스 전체가 체크되어야 버튼이 활성화 됩니다.
  • 각 항목을 클릭하면 웹뷰로 전환됩니다.
  • 웹뷰에서 다시 복귀했을 때 바텀시트를 유지합니다.
image
체크 안됨
image
전체 체크 완료

확인 방법

  • feature/login 에 PrivacyConsentBottomSheet를 구성하였습니다.

참고 사항

  • 로그인 성공 후 바텀시트를 열게 되는데 내리면 로그인도 실패처리를 해야하는지
  • 바텀시트가 내려가지 않게 해야하는지 이야기해보면 좋을 것 같습니다!
  • 개인정보 동의 상태를 따로 저장하거나 하지 않고 ui단에서만 처리하고 있습니다.
  • 현재는 동의 후 다음 버튼을 클릭하면 홈화면 으로 이동하게 되는데, 연락처 주기 설정 화면으로 구성하는 것은 feat/friend-contact-cycle 브랜치에서 담당합니다.

관련 이슈

- 로그인 성공, 에러 이벤트를 `LoginEvent` sealed class로 통합하여 관리
- 약관동의를 위한 ShowPrivacyBottomShhet Event 추가
- 사용자가 소셜 로그인 시, 약관에 동의할 수 있도록 바텀시트 UI를 구현했습니다.
- `PrivacyConsentBottomSheet`와 `TermsAgreementItem` Composable을 추가하여 약관 동의 화면을 구성했습니다.
- 로그인 성공 후 약관 동의 바텀시트가 나타나고, 동의 완료 시 홈 화면으로 이동하는 로직을 `LoginScreen`에 추가했습니다.
- `NearCheckbox` 컴포넌트의 클릭 효과에서 물결(ripple) 효과를 제거하기 위해 `clickable`을 `onNoRippleClick` 확장 함수로 변경했습니다.
- 약관 동의 상태를 관리하기 위한 `TermsAgreementState` 데이터 클래스와 `TermType` enum을 추가했습니다.
- `LoginViewModel`에 약관 전체 동의 및 개별 동의 토글 로직, 약관 상세 보기 이벤트 처리 로직을 구현했습니다.
- 필수 약관에 모두 동의해야 '가입' 버튼이 활성화되도록 수정했습니다.
- 약관 텍스트 영역 클릭 시 약관 상세 웹뷰로 이동하는 이벤트를 추가했습니다.
- `NearBottomSheetDragHandle` 컴포저블을 추가하여 BottomSheet의 드래그 핸들을 공통 컴포넌트로 분리했습니다.
- 소셜 로그인 성공 후 홈 화면으로 바로 이동하던 로직을 수정하여, 약관 동의 BottomSheet가 먼저 표시되도록 변경했습니다.
- 약관 동의가 완료된 후 홈 화면으로 이동하는 `onPrivacyConsentComplete` 함수를 추가했습니다.
- 로그인 성공, 에러 이벤트를 `LoginEvent` sealed class로 통합하여 관리
- 약관동의를 위한 ShowPrivacyBottomShhet Event 추가
- 사용자가 소셜 로그인 시, 약관에 동의할 수 있도록 바텀시트 UI를 구현했습니다.
- `PrivacyConsentBottomSheet`와 `TermsAgreementItem` Composable을 추가하여 약관 동의 화면을 구성했습니다.
- 로그인 성공 후 약관 동의 바텀시트가 나타나고, 동의 완료 시 홈 화면으로 이동하는 로직을 `LoginScreen`에 추가했습니다.
- `NearCheckbox` 컴포넌트의 클릭 효과에서 물결(ripple) 효과를 제거하기 위해 `clickable`을 `onNoRippleClick` 확장 함수로 변경했습니다.
- 약관 동의 상태를 관리하기 위한 `TermsAgreementState` 데이터 클래스와 `TermType` enum을 추가했습니다.
- `LoginViewModel`에 약관 전체 동의 및 개별 동의 토글 로직, 약관 상세 보기 이벤트 처리 로직을 구현했습니다.
- 필수 약관에 모두 동의해야 '가입' 버튼이 활성화되도록 수정했습니다.
- 약관 텍스트 영역 클릭 시 약관 상세 웹뷰로 이동하는 이벤트를 추가했습니다.
- `NearBottomSheetDragHandle` 컴포저블을 추가하여 BottomSheet의 드래그 핸들을 공통 컴포넌트로 분리했습니다.
- 소셜 로그인 성공 후 홈 화면으로 바로 이동하던 로직을 수정하여, 약관 동의 BottomSheet가 먼저 표시되도록 변경했습니다.
- 약관 동의가 완료된 후 홈 화면으로 이동하는 `onPrivacyConsentComplete` 함수를 추가했습니다.
- 우측 끝에 > 이미지를 추가합니다
- TermType에 titleRes, url 추가
- LoginScreen에서 약관 상세 보기 클릭 시 웹뷰로 이동하도록 수정
- PrivacyBottomSheet에서 TermType의 titleRes를 사용하도록 수정
- NearNavHost에 onNavigateToTerms 추가하여 웹뷰 이동 로직 연결
- `TermType` enum 클래스를 `LoginViewModel.kt`에서 `login/model/TermType.kt` 파일로 분리했습니다.
- `LoginScreen.kt`, `LoginViewModel.kt`, `PrivacyBottomSheet.kt`에서 분리된 `TermType` 클래스를 참조하도록 수정했습니다.
- `PrivacyBottomSheet.kt`에서 하드코딩된 문자열들을 `strings.xml`에 정의된 리소스로 변경했습니다.
- `strings.xml`에 개인정보 동의 바텀시트 관련 문자열들을 추가했습니다.
- LoginViewModel에서 약관 동의 및 바텀시트 상태를 LoginState로 통합 관리하도록 변경했습니다.
- 웹뷰에서 돌아올 때 개인정보처리방침 바텀시트가 다시 보이도록 로직을 추가했습니다.
- LoginScreen에서 Lifecycle 이벤트를 감지하여 바텀시트 복원 로직을 호출하도록 수정했습니다.
- `PrivacyBottomSheet.kt`에서 사용하던 약관 항목 `Triple`을 `TermsItem` 데이터 클래스로 분리했습니다.
…gin-privacy

# Conflicts:
#	Near/app/src/main/java/com/alarmy/near/presentation/feature/login/LoginScreen.kt
#	Near/app/src/main/java/com/alarmy/near/presentation/feature/login/LoginViewModel.kt
#	Near/app/src/main/java/com/alarmy/near/presentation/feature/login/PrivacyBottomSheet.kt
#	Near/app/src/main/java/com/alarmy/near/presentation/feature/login/components/NearBottomSheetDragHandle.kt
#	Near/app/src/main/java/com/alarmy/near/presentation/feature/login/components/TermsAgreementItem.kt
@gemini-code-assist
Copy link

Summary of Changes

Hello @stopstone, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

이 PR은 로그인 성공 후 사용자에게 개인정보 동의 절차를 도입하여 서비스 이용 전 필수 약관 동의를 받도록 합니다. 이를 위해 새로운 바텀시트 UI와 관련 로직을 구현했으며, 사용자가 약관 상세 내용을 웹뷰로 확인할 수 있도록 하고 웹뷰에서 복귀 시 바텀시트 상태를 유지하는 기능을 포함합니다. 전반적인 로그인 화면의 상태 관리도 개선되었습니다.

Highlights

  • 개인정보 동의 바텀시트 구현: 로그인 성공 후 사용자에게 개인정보 동의를 요청하는 바텀시트가 표시됩니다.
  • 약관 동의 로직: 모든 필수 약관에 동의해야 '가입' 버튼이 활성화되며, 개별 약관 및 전체 약관 동의 상태를 관리합니다.
  • 웹뷰 연동 및 상태 유지: 각 약관 항목 클릭 시 웹뷰로 상세 내용을 볼 수 있으며, 웹뷰에서 돌아왔을 때 바텀시트 상태가 유지됩니다.
  • ViewModel 상태 관리 개선: 로그인 관련 이벤트를 통합 관리하는 LoginEvent와 약관 동의 상태를 포함하는 LoginState를 도입하여 ViewModel의 상태 관리를 개선했습니다.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

로그인 성공 후 개인정보 동의를 받는 기능이 추가되었네요. 전체적으로 ViewModel에서 상태를 관리하고, Composable에서 이를 구독하여 UI를 그리는 패턴을 잘 따르고 있습니다. 특히 LoginEvent sealed class를 도입하여 이벤트를 하나로 통합한 점과, 웹뷰에서 돌아왔을 때 바텀시트 상태를 복원하는 로직을 구현한 점이 인상적입니다. 몇 가지 개선할 수 있는 부분에 대해 리뷰를 남겼으니 확인 부탁드립니다.

onNavigateToWebView(title, event.termType.url)
}

is LoginEvent.ShowError -> TODO()

Choose a reason for hiding this comment

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

high

로그인 에러 발생 시 처리가 TODO()로 남아있습니다. 이 경우 앱이 비정상적으로 동작하거나 사용자가 문제를 인지하지 못할 수 있습니다. LoginRoute에 에러 처리를 위한 콜백(예: onShowErrorSnackBar)을 추가하고, 이 곳에서 해당 콜백을 호출하여 사용자에게 스낵바 등으로 에러 상황을 알려주는 것이 좋습니다.

showPrivacyBottomSheet = false,
hasNavigatedToWebView = true,
)
delay(500)

Choose a reason for hiding this comment

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

medium

애니메이션 완료를 기다리기 위해 고정된 delay(500)를 사용하는 것은 불안정한 방법입니다. 사용자의 기기 성능이나 애니메이션 설정에 따라 UI가 깨져보일 수 있습니다.

가장 간단한 해결책은 delay를 제거하는 것입니다. 즉시 화면을 전환하더라도 showPrivacyBottomSheet 상태가 false로 변경되었기 때문에 바텀시트는 자연스럽게 사라집니다. 만약 전환 애니메이션이 꼭 필요하다면, Composable의 ModalBottomSheetState가 완전히 사라졌을 때(e.g., !bottomSheetState.isVisible) 이벤트를 발생시켜 화면을 전환하는 더 견고한 방법을 고려해볼 수 있습니다.

Comment on lines 144 to 181
val termsList = listOf(
TermsItem(
title = "$requiredPrefix ${stringResource(TermType.SERVICE_TERMS.titleRes)}",
termType = TermType.SERVICE_TERMS,
isAgreed = termsAgreementState.isServiceTermsAgreed,
),
TermsItem(
title = "$requiredPrefix ${stringResource(TermType.PRIVACY_COLLECTION.titleRes)}",
termType = TermType.PRIVACY_COLLECTION,
isAgreed = termsAgreementState.isPrivacyCollectionAgreed,
),
TermsItem(
title = "$requiredPrefix ${stringResource(TermType.PRIVACY_POLICY.titleRes)}",
termType = TermType.PRIVACY_POLICY,
isAgreed = termsAgreementState.isPrivacyPolicyAgreed,
),
)

Column(
modifier =
Modifier
.fillMaxWidth()
.border(
width = 1.dp,
color = NearTheme.colors.GRAY03_EBEBEB,
shape = RoundedCornerShape(12.dp),
).padding(horizontal = 16.dp, vertical = 4.dp),
) {
termsList.forEachIndexed { index, termsItem ->
TermsAgreementItem(
text = termsItem.title,
isChecked = termsItem.isAgreed,
onCheckedChange = { onToggleIndividualTerms(termsItem.termType) },
onclick = { onShowTermsDetail(termsItem.termType) },
showDivider = index < termsList.size - 1,
)
}
}

Choose a reason for hiding this comment

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

medium

TermsAgreementSection Composable이 리컴포지션될 때마다 termsList가 새로 생성되고 있습니다. 여기에는 stringResource 호출과 객체 생성이 포함되어 있어 비효율적일 수 있습니다.

약관 타입(TermType) 목록을 상수로 정의하거나 remember로 감싼 뒤 forEachIndexed 루프 안에서 TermsAgreementItem을 직접 생성하는 방식으로 리팩토링하여 불필요한 객체 생성을 줄이는 것을 제안합니다.

    val terms = listOf(
        TermType.SERVICE_TERMS,
        TermType.PRIVACY_COLLECTION,
        TermType.PRIVACY_POLICY,
    )

    Column(
        modifier =
            Modifier
                .fillMaxWidth()
                .border(
                    width = 1.dp,
                    color = NearTheme.colors.GRAY03_EBEBEB,
                    shape = RoundedCornerShape(12.dp),
                ).padding(horizontal = 16.dp, vertical = 4.dp),
    ) {
        terms.forEachIndexed { index, termType ->
            val isAgreed =
                when (termType) {
                    TermType.SERVICE_TERMS -> termsAgreementState.isServiceTermsAgreed
                    TermType.PRIVACY_COLLECTION -> termsAgreementState.isPrivacyCollectionAgreed
                    TermType.PRIVACY_POLICY -> termsAgreementState.isPrivacyPolicyAgreed
                }
            TermsAgreementItem(
                text = "$requiredPrefix ${stringResource(termType.titleRes)}",
                isChecked = isAgreed,
                onCheckedChange = { onToggleIndividualTerms(termType) },
                onclick = { onShowTermsDetail(termType) },
                showDivider = index < terms.size - 1,
            )
        }
    }

Comment on lines +30 to +58
Row(
modifier =
Modifier
.padding(vertical = 14.dp)
.onNoRippleClick(onclick),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
// 체크박스 클릭 시 체크 상태만 변경
NearCheckbox(
checked = isChecked,
onCheckedChange = onCheckedChange,
)

Spacer(modifier = Modifier.size(8.dp))

// 텍스트 영역 클릭 시 웹뷰로 이동
Text(
text = text,
style = NearTheme.typography.B2_14_MEDIUM,
)

Spacer(modifier = Modifier.weight(1f))

Image(
painter = painterResource(R.drawable.ic_front_24_gray),
contentDescription = null,
)
}

Choose a reason for hiding this comment

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

medium

Row 전체에 onNoRippleClick을 적용하면 체크박스 주변의 빈 공간을 클릭해도 웹뷰로 이동하게 됩니다. 이는 사용자가 의도하지 않은 동작일 수 있습니다.

클릭 영역을 명확히 하기 위해, Text와 화살표 Image를 별도의 Row로 감싸고 해당 Row에만 onNoRippleClick을 적용하는 것을 고려해 보세요. 이렇게 하면 사용자가 웹뷰로 이동하는 영역을 더 직관적으로 인지할 수 있습니다.

    Row(
        modifier = Modifier.padding(vertical = 14.dp),
        verticalAlignment = Alignment.CenterVertically,
    ) {
        // 체크박스 클릭 시 체크 상태만 변경
        NearCheckbox(
            checked = isChecked,
            onCheckedChange = onCheckedChange,
        )

        Spacer(modifier = Modifier.size(8.dp))

        // 텍스트와 화살표 영역 클릭 시 웹뷰로 이동
        Row(
            modifier = Modifier.weight(1f).onNoRippleClick(onclick),
            verticalAlignment = Alignment.CenterVertically,
        ) {
            Text(
                text = text,
                style = NearTheme.typography.B2_14_MEDIUM,
            )

            Spacer(modifier = Modifier.weight(1f))

            Image(
                painter = painterResource(R.drawable.ic_front_24_gray),
                contentDescription = null,
            )
        }
    }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

현재 Row 자체를 클릭하면 웹뷰로 전환되도록 의도하여 이부분은 수정하지 않겠습니다.
체크박스 부분은 웹뷰로 전환되지 않고 체크만 되고, 다른 Row영역은 클릭되는 것이 맞습니다.

- `TermsItem` 데이터 클래스 사용을 제거하고, `TermType` enum class를 직접 사용하여 약관 항목을 구성하도록 수정했습니다.
- 약관 타입 목록(`terms`)을 `remember`를 사용하여 리컴포지션 시 재생성되지 않도록 최적화했습니다.
- `LoginRoute`에 `onShowErrorSnackBar` 콜백을 추가하여 로그인 실패 시 스낵바를 표시하도록 구현했습니다.
- `LoginEvent.ShowError` 발생 시 `onShowErrorSnackBar`를 호출하여 에러 메시지를 전달합니다.
- `LoginScreen`에서 약관 클릭 시 웹뷰로 이동하는 `onNavigateToWebView` 콜백을 `PrivacyBottomSheet`에 전달하도록 수정했습니다.
- 기존 delay()를 활용한 이유는 바텀시트가 내려가기 전에 웹뷰로 전환되어 딜레이를 호출하였습니다.
- `WebViewFrame.kt`에서 웹뷰 페이지 로딩 중 `CircularProgressIndicator`를 표시하도록 수정했습니다.
- 웹뷰 로딩 상태를 관리하기 위해 `isLoading` 상태 변수를 추가했습니다.
- `WebViewClient`의 `onPageStarted` 및 `onPageFinished` 콜백을 사용하여 로딩 상태를 업데이트합니다.
Copy link
Contributor

@rhkrwngud445 rhkrwngud445 left a comment

Choose a reason for hiding this comment

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

고생하셨습니다! viewModel 컴포저블 파라미터 관련해서 코멘트 남겼는데, 이것만 확인 부탁드려요!

onDismiss: () -> Unit,
onConsentComplete: () -> Unit,
onTermsClick: (TermType) -> Unit = {},
viewModel: LoginViewModel,
Copy link
Contributor

Choose a reason for hiding this comment

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

viewModel을 인자로 받아오는 것보다 상태를 호이스팅하는 것은 어떨까요? 해당 컴포저블은 preview 작성이 어려워 보이네요!

- `PrivacyBottomSheet.kt`에서 ViewModel을 직접 참조하지 않고, `LoginScreen.kt`를 통해 상태와 이벤트를 전달받도록 수정했습니다.
- `LoginScreen.kt`에서 `termsAgreementState`를 수집하고 `PrivacyBottomSheet`에 전달하도록 변경했습니다.
- PrivacyBottomSheet의 컨텐츠를 `PrivacyConsentBottomSheetContent` Composable 함수로 분리했습니다.
- 분리된 `PrivacyConsentBottomSheetContent`에 대한 프리뷰를 추가했습니다.
- `PrivacyConsentBottomSheet` 자체를 프리뷰로 그리면 바텀시트가 표시되지 않는 현상이 있어 Content로 따로 분리하였습니다
@stopstone stopstone merged commit cc0e720 into dev Sep 28, 2025
1 check passed
@stopstone stopstone deleted the feat/login-privacy branch October 6, 2025 11:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] 개인정보 동의 및 확인

3 participants