Skip to content

카카오 로그인 연동 구조 설계 및 초기 구현#86

Merged
HamBeomJoon merged 45 commits intodevelopfrom
#79-kakao-login
Mar 30, 2026
Merged

카카오 로그인 연동 구조 설계 및 초기 구현#86
HamBeomJoon merged 45 commits intodevelopfrom
#79-kakao-login

Conversation

@HamBeomJoon
Copy link
Copy Markdown
Contributor

@HamBeomJoon HamBeomJoon commented Mar 27, 2026

📌 작업 내용

  • 카카오 SDK 의존과 초기화 책임을 app/feature에서 분리해 core:auth로 이동
  • app은 카카오 SDK를 직접 모르고 KakaoAuthInitializer만 호출하도록 정리
  • feature/login은 KakaoLoginManager를 라우트에서 주입받아 사용하고, LoginUiIntent에서 Context 제거
  • LoginViewModel은 onIntent, uiState, uiEffect 기반의 단순한 MVI 형태로 정리하고 상태 변경은 reduce로 통일
  • 로그인 성공 후 홈 전환 시 isNavigatingToHome 로컬 상태로 버튼을 먼저 숨긴 뒤 이동하도록 조정
  • 로그인 화면의 로컬 스낵바 호스트를 제거하고 앱 공통 SnackbarHostState 사용으로 통일
  • 카카오 로그인 실패/429(rate limit) 케이스를 구분해 사용자 메시지를 개선하고 Timber 로그 추가

🧩 관련 이슈

close #79

📸 스크린샷

Screenshot_1773650487
  • 카카오 로그인 성공후 accessToken받아옴
스크린샷 2026-03-16 17 42 33

📢 논의하고 싶은 내용

Summary by CodeRabbit

  • 새로운 기능

    • Kakao 로그인 기능 추가
    • 선택적 작업 버튼이 포함된 향상된 스낵바
    • 실수로 인한 중복 클릭 방지 기능
  • 개선 사항

    • 네비게이션 바 인디케이터 투명 처리 및 리플 효과 제거
    • Kakao 통합이 적용된 로그인 화면 완전 재설계
    • 스낵바 작업 레이블 옵션화

- `feature:login:api` 및 `feature:login:impl` 모듈 생성 및 프로젝트 등록
- `LoginNavKey` 정의 및 `SharedTransitionScope`를 활용한 `LoginScreen` UI 구현
- `LoginViewModel`, `LoginUiState`, `LoginUiEffect`를 통한 로그인 비즈니스 로직 및 상태 관리 추가
- Hilt Multi-bindings (`IntoSet`) 기반의 `LoginEntryBuilder` 네비게이션 등록 방식 적용
- `AndroidFeatureImplConventionPlugin`에 `hilt-lifecycle-viewmodel-compose` 의존성 추가
- 디자인 시스템에 `core_designsystem_logo_prezel` 로고 에셋 추가
- `app` 모듈에 신규 로그인 피처 의존성 및 `kotlinx-serialization` 플러그인 추가
# Conflicts:
#	Prezel/core/designsystem/src/main/res/drawable/core_designsystem_logo_prezel.xml
#	Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginViewModel.kt
#	Prezel/settings.gradle.kts
`PrezelNavigationBar`의 사용자 경험과 디자인 일관성을 높이기 위해 클릭 피드백을 제거하고 스타일을 조정했습니다.

* **리플 효과 제거**: `NoRippleInteractionSource`를 추가하고 `PrezelNavigationBarItem`에 적용하여 클릭 시 발생하는 리플 애니메이션을 비활성화했습니다.
* **색상 수정**: 선택된 아이템의 강조 표시 배경색(`indicatorColor`)을 `bgRegular`에서 `Transparent`로 변경했습니다.
* **명명 규칙 변경**: `PrezelNavigationBarScope` 내의 `item` 함수를 대문자로 시작하는 `Item`으로 변경하여 컴포저블 컨벤션을 준수했습니다.
* **유틸리티 추가**: `MutableInteractionSource`를 상속받아 상호작용을 무시하는 `NoRippleInteractionSource` 객체를 추가했습니다.
`PrezelSnackbar`에서 액션 버튼(`actionLabel`)이 없을 경우에도 텍스트만 표시(Toast 형태)할 수 있도록 기능을 개선했습니다.

*   `PrezelSnackbar`: `visuals.actionLabel`이 `null`인 경우 액션 버튼과 간격(`Spacer`)을 렌더링하지 않도록 수정
*   `SnackbarHost`: `PrezelSnackbarVisuals`의 `actionLabel` 타입을 옵셔널(`String?`)로 변경
*   `showPrezelSnackbar`:
    *   `actionLabel` 및 `onAction` 파라미터를 선택 사항으로 변경
    *   `actionLabel`과 `onAction`이 동시에 존재하거나 둘 다 없어야 함을 보장하는 `require` 구문 추가
*   `Preview`: 액션 버튼이 없는 경우를 위한 `PrezelToastPreview` 추가 및 기존 프리뷰 데이터 구조 수정
* feat: Kakao SDK 의존성 추가 및 초기화 로직 구현
    * `libs.versions.toml`에 Kakao SDK 버전 및 `kakao-user` 라이브러리 추가
    * `settings.gradle.kts`에 Kakao Maven 저장소 등록
    * `PrezelApplication`에서 `KakaoSdk.init`을 통한 SDK 초기화 로직 추가

* feat: Kakao Native App Key 설정 및 Manifest 등록
    * `app/build.gradle.kts`에서 프로젝트 속성의 `KAKAO_NATIVE_APP_KEY`를 읽어 `BuildConfig` 및 `manifestPlaceholders`에 설정
    * `AndroidManifest.xml`에 카카오 로그인을 위한 `AuthCodeHandlerActivity` 등록 및 스킴 설정

* build: 모듈별 의존성 추가
    * `app` 및 `feature:login:impl` 모듈에 `kakao-user` 의존성 추가

* refactor: PrezelApp 내 네비게이션 아이템 호출부 수정
    * `PrezelApp.kt`에서 네비게이션 아이템 생성 시 `item` 호출을 대문자 `Item`으로 수정 (컴포넌트 명칭 변경 대응)
중복 이벤트 발생을 방지하기 위해 일정 시간 내의 연속된 클릭을 제한하는 `ComposeMultipleEventCutter` 객체와 확장 함수를 추가했습니다.

*   `clickOnce`: 파라미터 유무에 따른 클릭 이벤트 중복 방지 확장 함수 추가
*   `ComposeMultipleEventCutter`: 마지막 이벤트 발생 시간을 기준으로 이벤트 실행 여부를 판단하는 내부 객체 구현
중복 이벤트 발생을 방지하기 위해 일정 시간 내의 연속된 클릭을 제한하는 `ComposeMultipleEventCutter` 객체와 확장 함수를 추가했습니다.

*   `clickOnce`: 파라미터 유무에 따른 클릭 이벤트 중복 방지 확장 함수 추가
*   `ComposeMultipleEventCutter`: 마지막 이벤트 발생 시간을 기준으로 이벤트 실행 여부를 판단하는 내부 객체 구현
* `KakaoLoginManager`: 카카오 SDK를 활용하여 로그인(카카오톡/카카오계정) 및 로그아웃 기능을 처리하는 클래스 추가
* `KakaoLoginResult`: 로그인 성공 시 액세스 토큰을 반환하고 실패 시 에러를 전달하는 sealed interface 정의
* `suspendCancellableCoroutine`을 사용하여 비동기 SDK 콜백 로직을 코루틴 기반으로 구현
카카오 로그인 기능을 추가하고, 기존의 단순했던 로그인 로직을 `LoginUiIntent`, `LoginUiState`, `LoginUiEffect`를 활용한 MVI 패턴으로 리팩토링했습니다.

* **feat: 카카오 로그인 연동 및 상태 관리 개선**
    * `LoginUiIntent`를 추가하여 로그인 클릭, 성공, 실패 액션을 정의했습니다.
    * `LoginUiState`를 `sealed interface`에서 `data class`로 변경하고 `isLoading` 상태를 관리하도록 수정했습니다.
    * `LoginUiEffect`에 `LaunchKakaoLogin` 및 `ShowSnackbar` 효과를 추가했습니다.
    * `LoginViewModel`에서 `onIntent` 메서드를 통해 비즈니스 로직을 처리하도록 구현했습니다.

* **feat: 로그인 화면 UI 업데이트 및 스낵바 추가**
    * `KakaoLoginManager`를 연동하여 실제 카카오 로그인 프로세스를 호출합니다.
    * 로그인 버튼을 디자인 시스템의 `PrezelButtonArea` 및 카카오 아이콘을 사용한 '카카오로 시작하기' 버튼으로 교체했습니다.
    * 로그인 실패 시 에러 메시지를 표시하기 위한 `SnackbarHost` 및 `PrezelSnackbar`를 추가했습니다.
    * 중복 클릭 방지를 위해 `clickOnce` 확장 함수를 적용하고 `isLoading` 상태에 따른 버튼 활성화 로직을 추가했습니다.

* **refactor: 로그인 화면 레이아웃 및 코드 구조 정리**
    * `LoginScreen`의 최상위 레이아웃을 `Box`로 변경하여 스낵바 위치를 조정했습니다.
    * 불필요한 `fillMaxWidth` 및 패딩 설정을 정리하고 프리뷰 코드를 업데이트했습니다.
`LoginScreen` 내에서 사용되던 카카오 로그인 실패 메시지를 문자열 리소스로 분리하고, 레이아웃 한정자 및 컴포저블 구조를 정리했습니다.

*   `strings.xml`: 카카오 로그인 실패 메시지(`feature_login_impl_kakao_failure`) 추가
*   `LoginScreen.kt`:
    *   `stringResource`를 사용하여 로그인 실패 메시지 적용
    *   `Column`의 `modifier` 파라미터 중복 참조 수정 (`modifier` -> `Modifier`)
    *   `SnackbarHost` 내부 람다 형식 및 코드 정렬 수정
`app/build.gradle.kts`에서 `KAKAO_NATIVE_APP_KEY`를 가져오는 방식을 `project.findProperty`에서 `local.properties` 파일을 직접 로드하여 읽어오는 방식으로 수정했습니다. 키가 존재하지 않을 경우 에러를 발생시키도록 변경하여 빌드 시점의 안정성을 강화했습니다.
`LoginViewModel`의 내부 로직을 정리하고, 외부에서 직접 호출 가능한 메서드를 도입하여 네비게이션 및 결과 처리 구조를 개선했습니다.

* **LoginViewModel**:
    * `onIntent`에서 처리하던 `LoginSucceeded`, `LoginFailed` 로직을 별도의 public 메서드인 `onLoginSuccess()`, `onLoginFailure()`로 분리했습니다.
    * 불필요한 들여쓰기를 수정하고 코드를 정리했습니다.
* **LoginUiIntent**: 더 이상 사용되지 않는 `LoginSucceeded`, `LoginFailed` intent를 제거했습니다.
* **LoginScreen**: 카카오 로그인 결과에 따라 `viewModel.onLoginSuccess()` 또는 `viewModel.onLoginFailure()`를 직접 호출하도록 수정했습니다.
* **기타**:
    * `feature:login:api` 모듈의 불필요한 `AndroidManifest.xml`을 삭제했습니다.
    * `feature:login:impl` 모듈의 `AndroidManifest.xml` 형식을 간소화했습니다.
# Conflicts:
#	Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelNavigationBar.kt
#	Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/snackbar/PrezelSnackbar.kt
#	Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/snackbar/SnackbarHost.kt
#	Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/LoginScreen.kt
*   **core:designsystem**: `PrezelSnackbar`에서 `actionLabel`이 없을 경우 버튼을 렌더링하지 않도록 방어 로직을 추가하고, 관련 프리뷰 코드를 `BasicPreview` 및 `PreviewSection` 구조로 개선했습니다.
*   **core:designsystem**: `showPrezelSnackbar` 확장 함수에서 불필요하게 복잡했던 파라미터 정의와 `require` 제약 조건을 제거하여 호출부를 단순화했습니다.
*   **feature:login**: `LoginScreen` 내 스낵바 호출 시 누락된 액션 파라미터를 추가하고, 사용하지 않는 `PrezelButtonArea` 관련 코드 및 임포트를 정리했습니다.
*   **style**: `PrezelNavigationBar` 내 불필요한 공백 및 임포트 순서를 정리했습니다.
* **feat: 카카오 로그인 버튼 추가**
    * `LoginFooter` 내부에 카카오 브랜드 색상(`0xFFFEE500`)이 적용된 `PrezelButtonArea` 및 커스텀 버튼을 구현했습니다.
    * `PrezelIcons.Kakao` 아이콘과 "카카오로 시작하기" 라벨을 적용했습니다.
    * `isLoading` 상태에 따른 버튼 활성화 로직을 추가했습니다.

* **refactor: 로그인 화면 레이아웃 및 애니메이션 구조 개선**
    * `LoginScreen`의 전체 구조를 `Column`에서 `Box`로 변경하여 로고와 푸터의 배치를 최적화했습니다.
    * 로고 이미지가 화면 중앙에 올바르게 위치하도록 `Alignment.Center` 및 `Box` 래퍼를 추가했습니다.
    * `SharedElement` 전환 효과가 적용되는 로고 이미지의 레이아웃 제약 조건을 수정했습니다.

* **refactor: ButtonAreaScope 내부 로직 수정**
    * `validateButtonCount`에서 버튼이 2개인 경우 리스트를 초기화하는 로직을 추가하여 버튼 선언 제한 로직을 보완했습니다.

* **test: 미리보기(Preview) 추가**
    * 로딩 상태를 확인할 수 있는 `LoginScreenLoadingPreview`를 추가했습니다.
# Conflicts:
#	Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/LoginScreen.kt
* `LoginScreen` 내 카카오 로그인 버튼의 `PrezelButtonConfig`를 별도 변수(`kakaoButtonConfig`)로 분출하여 가독성을 개선했습니다.
* `PrezelButtonArea`에서 불필요한 `isNested = true` 파라미터를 제거했습니다.
* 사용하지 않는 import문을 정리했습니다.
@HamBeomJoon HamBeomJoon requested a review from moondev03 as a code owner March 27, 2026 03:44
@HamBeomJoon HamBeomJoon self-assigned this Mar 27, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 27, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

카카오 SDK를 앱에 통합하고 core/data 모듈에 KakaoLoginManager 인터페이스 및 구현을 추가했습니다. 로그인 화면을 업데이트하여 카카오 로그인 버튼을 추가하고, Design System 컴포넌트를 개선하여 ripple 효과 제거, snackbar 액션 레이블 선택화, 이벤트 필터링을 지원합니다.

Changes

Cohort / File(s) Summary
Kakao SDK 초기화 및 설정
Prezel/app/build.gradle.kts, Prezel/app/src/main/AndroidManifest.xml, Prezel/app/src/main/java/com/team/prezel/PrezelApplication.kt, Prezel/gradle/libs.versions.toml, Prezel/settings.gradle.kts
local.properties에서 kakao.native.app.key를 로드하여 BuildConfig 필드 및 manifest placeholder로 설정했습니다. Kakao SDK 저장소를 settings.gradle.kts에 추가하고, PrezelApplication에서 KakaoSdk.init()을 호출합니다. AuthCodeHandlerActivity를 manifest에 등록하여 OAuth 콜백을 처리합니다.
Kakao 로그인 핵심 인터페이스 및 구현
Prezel/core/data/src/main/java/com/team/prezel/core/data/auth/KakaoLoginManager.kt, Prezel/core/data/src/main/java/com/team/prezel/core/data/auth/KakaoLoginManagerImpl.kt, Prezel/core/data/src/main/java/com/team/prezel/core/data/auth/KakaoLoginResult.kt, Prezel/core/data/src/main/java/com/team/prezel/core/data/di/RepositoryModule.kt
KakaoLoginManager 인터페이스를 정의하고, KakaoLoginManagerImpl에서 Kakao Talk/계정 로그인을 지원합니다. 로그인 결과를 Success, RateLimited, Failure로 분류합니다. Hilt에서 싱글톤으로 바인딩됩니다.
로그인 기능 UI/ViewModel 업데이트
Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/LoginScreen.kt, Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginViewModel.kt, Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginUiIntent.kt, Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginUiState.kt, Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginUiEffect.kt, Prezel/feature/login/impl/src/main/res/values/strings.xml
LoginViewModel에서 KakaoLoginManager를 주입받아 로그인을 처리합니다. Intent 기반 아키텍처로 변경하고, 로그인 상태를 isLoading 플래그로 단순화했습니다. LoginScreen에 카카오 로그인 버튼을 추가하고, snackbar 표시 기능을 통합합니다. 실패 및 rate limit 메시지 문자열을 추가했습니다.
Design System 컴포넌트 개선
Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelNavigationBar.kt, Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/snackbar/PrezelSnackbar.kt, Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/snackbar/SnackbarHost.kt, Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/util/ComposeMultipleEventCutter.kt, Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/util/NoRippleInteractionSource.kt
Navigation bar 아이템의 indicator 색상을 투명으로 설정하고 ripple 효과를 제거했습니다. Snackbar의 actionLabel을 nullable로 변경하고 조건부 렌더링을 지원합니다. 이벤트 연속 클릭 필터링(clickOnce)과 ripple 미적용 InteractionSource를 추가했습니다.
모듈 의존성 및 빌드 설정
Prezel/feature/login/impl/build.gradle.kts, Prezel/feature/login/impl/src/main/AndroidManifest.xml, Prezel/core/data/build.gradle.kts, Prezel/core/network/build.gradle.kts
feature/login/impl에서 core/data 의존성을 추가하고, feature 모듈의 manifest를 생성했습니다. core/data에 kakao.user와 timber 의존성을 추가했습니다. core/network에서 BASE_URL 속성명 형식을 변경했습니다.
기타 업데이트
Prezel/app/src/main/java/com/team/prezel/ui/PrezelApp.kt
PrezelAppContent에서 shouldShowNavigationBar 값을 로컬 변수로 캐싱하여 전달합니다.

Possibly related PRs

Suggested labels

✨ feat

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 카카오 로그인 연동 구조 설계 및 초기 구현으로, 변경사항의 핵심 내용을 명확하게 요약하고 있습니다.
Linked Issues check ✅ Passed PR의 모든 코드 변경사항이 #79 이슈의 요구사항을 충족합니다: 카카오 로그인 연동 구현 및 feature 모듈(UI)과 core:data 계층(비즈니스 로직/SDK) 분리 아키텍처가 잘 구현되어 있습니다.
Out of Scope Changes check ✅ Passed PR의 모든 변경사항이 카카오 로그인 연동 관련 범위 내에 있으며, 불필요한 범위 외 변경사항은 없습니다.
Description check ✅ Passed PR 설명이 템플릿 구조를 따르며 작업 내용, 관련 이슈, 스크린샷, 논의 사항을 포함하고 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🧹 Nitpick comments (8)
Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/util/ComposeMultipleEventCutter.kt (1)

25-29: 이벤트 차단 시에도 lastEventTimeMs 갱신으로 디바운스 시간 연장

현재 로직은 이벤트 실행 여부와 관계없이 lastEventTimeMs를 항상 갱신합니다. 사용자가 빠르게 연속 클릭하면 디바운스 시간이 계속 연장되어 정상적인 클릭도 무기한 차단될 수 있습니다.

의도된 동작인지 확인이 필요합니다. 일반적인 디바운스는 이벤트가 실행될 때만 타임스탬프를 갱신합니다.

♻️ 이벤트 실행 시에만 타임스탬프 갱신
     fun processEvent(
         time: Long,
         event: () -> Unit,
     ) {
         if (now - lastEventTimeMs >= time) {
             event.invoke()
+            lastEventTimeMs = now
         }
-
-        lastEventTimeMs = now
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/util/ComposeMultipleEventCutter.kt`
around lines 25 - 29, 현재 ComposeMultipleEventCutter 내에서 이벤트 실행 여부와 상관없이
lastEventTimeMs를 항상 갱신해 빠른 연속 클릭 시 디바운스가 계속 연장되는 문제가 있습니다; if 검사로 이벤트를 실행하는
위치(조건문: if (now - lastEventTimeMs >= time) { event.invoke() }) 안으로
lastEventTimeMs = now 할당을 옮겨 이벤트가 실제로 실행될 때만 타임스탬프를 갱신하도록 변경하세요.
Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/snackbar/PrezelSnackbar.kt (1)

66-73: 액션 버튼의 조건부 렌더링이 잘 구현되었습니다.

actionLabel이 null일 때 버튼을 렌더링하지 않는 로직이 적절합니다. 다만, let 블록 내에서 it을 사용하면 더 간결해집니다.

♻️ 선택적 리팩토링 제안
             visuals.actionLabel?.let {
                 PrezelButton(
-                    text = visuals.actionLabel,
+                    text = it,
                     type = ButtonType.GHOST,
                     size = ButtonSize.SMALL,
                     onClick = { data.performAction() },
                 )
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/snackbar/PrezelSnackbar.kt`
around lines 66 - 73, The conditional rendering is fine but the let block
redundantly references visuals.actionLabel; change the lambda to use the
implicit it parameter instead of re-reading visuals.actionLabel so the
PrezelButton call uses it for text (and keep ButtonType.GHOST, ButtonSize.SMALL
and onClick = { data.performAction() } unchanged), i.e. update the
visuals.actionLabel?.let { ... } block to reference it inside the PrezelButton
call.
Prezel/app/build.gradle.kts (1)

9-15: local.properties 로드 시 파일 존재 여부 확인 권장

local.properties 파일이 존재하지 않을 경우 FileNotFoundException이 발생합니다. CI 환경이나 신규 클론 시 더 명확한 에러 메시지를 제공하면 디버깅이 용이합니다.

♻️ 파일 존재 여부 확인 추가
 val localProperties = Properties().apply {
-    load(rootProject.file("local.properties").inputStream())
+    val localPropertiesFile = rootProject.file("local.properties")
+    if (localPropertiesFile.exists()) {
+        load(localPropertiesFile.inputStream())
+    }
 }

 val kakaoNativeAppKey =
     localProperties.getProperty("KAKAO_NATIVE_APP_KEY")
         ?: error("KAKAO_NATIVE_APP_KEY가 local.properties에 없습니다.")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Prezel/app/build.gradle.kts` around lines 9 - 15, Check for existence of the
local.properties file before trying to load it: guard the
rootProject.file("local.properties") call used to populate localProperties and
throw or log a clear error if the file doesn't exist (instead of letting
load(...) raise FileNotFoundException), then continue to read
KAKAO_NATIVE_APP_KEY into kakaoNativeAppKey as before; update the error message
for missing KAKAO_NATIVE_APP_KEY to remain clear.
Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/kakao/KakaoLoginManager.kt (2)

10-29: 코루틴 취소 시 안전성 고려 필요

suspendCancellableCoroutine을 사용하지만 취소 처리가 없습니다. 코루틴이 취소된 후에도 카카오 SDK 콜백이 실행되어 이미 취소된 continuation을 resume하려 할 수 있습니다. isActive 체크를 추가하면 더 안전합니다.

♻️ 취소 안전성 개선
     suspend fun login(context: Context): KakaoLoginResult =
         suspendCancellableCoroutine { continuation ->
             val callback: (OAuthToken?, Throwable?) -> Unit = { token, error ->
+                if (!continuation.isActive) return@Unit
                 when {
                     error != null -> continuation.resume(KakaoLoginResult.Failure(error))
                     token != null -> continuation.resume(KakaoLoginResult.Success(token.accessToken))
                     else -> continuation.resume(
                         KakaoLoginResult.Failure(
                             IllegalStateException("카카오 로그인 결과가 비어있습니다."),
                         ),
                     )
                 }
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/kakao/KakaoLoginManager.kt`
around lines 10 - 29, The login function uses suspendCancellableCoroutine but
doesn't guard against resuming a cancelled continuation; modify the callback
lambda (the one assigned to callback in login) to check continuation.isActive
before calling continuation.resume (for Success/Failure) and ensure you register
continuation.invokeOnCancellation to clean up or cancel any ongoing SDK
operation; update both call sites UserApiClient.instance.loginWithKakaoTalk(...)
and UserApiClient.instance.loginWithKakaoAccount(...) to rely on this guarded
callback so the suspended coroutine won’t be resumed after cancellation.

9-41: Hilt를 통한 의존성 주입 고려

KakaoLoginManager는 테스트 용이성과 의존성 관리를 위해 Hilt로 주입하는 것이 좋습니다. 현재 LoginScreen에서 직접 인스턴스를 생성하는 것으로 보입니다.

♻️ Hilt 모듈로 제공
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
-class KakaoLoginManager {
+class KakaoLoginManager `@Inject` constructor() {
     suspend fun login(context: Context): KakaoLoginResult =
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/kakao/KakaoLoginManager.kt`
around lines 9 - 41, KakaoLoginManager is instantiated directly in UI which
hinders testing and DI; make it injectable via Hilt by converting it to accept
its dependencies in the constructor (e.g., UserApiClient or a small interface
like KakaoAuthClient) and annotate the class with `@Singleton` and an `@Inject`
constructor (or provide a `@Provides/`@Binds in a Hilt module if you prefer an
interface). Update usages (e.g., LoginScreen) to obtain KakaoLoginManager via
`@Inject` or hiltViewModel, and ensure methods login and logout continue to
delegate to the injected UserApiClient/KakaoAuthClient; add a Hilt module only
if you need to adapt the existing UserApiClient into a bindable implementation.
Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginViewModel.kt (2)

31-47: onLoginSuccess/onLoginFailure 중복 호출 방지 고려

onLoginSuccess()onLoginFailure()가 public이고 isLoading 상태와 무관하게 호출 가능합니다. 로그인 결과 처리가 중복 실행되는 것을 방지하려면 가드를 추가하거나 internal 가시성을 고려해 보세요.

♻️ 중복 호출 방지 가드 추가
         fun onLoginSuccess() {
+            if (!_uiState.value.isLoading) return
             viewModelScope.launch {
                 _uiState.update { it.copy(isLoading = false) }
                 _uiEffect.send(LoginUiEffect.NavigateToHome)
             }
         }

         fun onLoginFailure(message: String?) {
+            if (!_uiState.value.isLoading) return
             viewModelScope.launch {
                 _uiState.update { it.copy(isLoading = false) }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginViewModel.kt`
around lines 31 - 47, onLoginSuccess and onLoginFailure are public and can be
invoked repeatedly regardless of current state; add a guard or reduce visibility
to prevent duplicate handling: either change the functions' visibility to
internal or, inside onLoginSuccess/onLoginFailure, early-return unless the
current uiState indicates a login is in-progress (check
_uiState.value.isLoading), then set isLoading=false and send the ui effect.
Reference the functions onLoginSuccess/onLoginFailure, the _uiState state
property, _uiEffect, and the effects LoginUiEffect.NavigateToHome and
LoginUiEffect.ShowSnackbar when making the change.

42-44: 하드코딩된 문자열을 리소스로 분리 권장

"카카오 로그인에 실패했습니다. 다시 시도해주세요." 기본 메시지가 하드코딩되어 있습니다. 다국어 지원 및 일관성을 위해 string resource 사용을 권장합니다. 이미 strings.xmlfeature_login_impl_kakao_failure가 있는 것으로 보이니 해당 리소스를 활용하세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginViewModel.kt`
around lines 42 - 44, Replace the hardcoded default message in the
LoginViewModel emission of LoginUiEffect.ShowSnackbar with the string resource
feature_login_impl_kakao_failure; do this by removing the literal fallback ("카카오
로그인에 실패했습니다. 다시 시도해주세요.") and fetching the resource instead — either accept a
localized message from the UI caller, or inject/use a
ResourceProvider/StringProvider into LoginViewModel to call
getString(R.string.feature_login_impl_kakao_failure) when message is null, and
then pass that resource-derived text into LoginUiEffect.ShowSnackbar.
Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/snackbar/SnackbarHost.kt (1)

24-48: actionLabelonAction 파라미터의 nullable 일관성 문제

PrezelSnackbarVisuals.actionLabelString?으로 nullable하게 변경되었지만, showPrezelSnackbar 함수의 actionLabel: StringonAction: () -> Unit은 여전히 non-null입니다.

Line 46의 onAction?.invoke()onAction이 non-nullable이므로 safe call이 불필요합니다. Toast 스타일(액션 없는 스낵바)을 지원하려면 이 함수도 nullable 파라미터를 받도록 수정하거나, 별도의 오버로드 함수를 추가하는 것이 좋습니다.

♻️ Toast 스타일 지원을 위한 오버로드 함수 추가 제안
+suspend fun SnackbarHostState.showPrezelSnackbar(
+    message: String,
+    `@DrawableRes` leadingIconResId: Int? = null,
+    duration: SnackbarDuration = SnackbarDuration.Short,
+    id: String? = null,
+    onDismiss: (() -> Unit)? = null,
+    offsetY: Dp = 0.dp,
+) {
+    val result = showSnackbar(
+        visuals = PrezelSnackbarVisuals(
+            message = message,
+            actionLabel = null,
+            duration = duration,
+            id = id,
+            leadingIconResId = leadingIconResId,
+            offsetY = offsetY,
+        ),
+    )
+
+    if (result == SnackbarResult.Dismissed) {
+        onDismiss?.invoke()
+    }
+}
+
 suspend fun SnackbarHostState.showPrezelSnackbar(
     message: String,
     actionLabel: String,
     onAction: () -> Unit,
     ...
 ) {
     ...
     when (result) {
-        SnackbarResult.ActionPerformed -> onAction?.invoke()
+        SnackbarResult.ActionPerformed -> onAction.invoke()
         SnackbarResult.Dismissed -> onDismiss?.invoke()
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/snackbar/SnackbarHost.kt`
around lines 24 - 48, 함수 SnackbarHostState.showPrezelSnackbar에서 현재 actionLabel:
String 및 onAction: () -> Unit의 non-null 시그니처가 PrezelSnackbarVisuals.actionLabel:
String?과 불일치하므로, toast(액션 없음) 스타일을 지원하려면 actionLabel을 String?으로, onAction를 (()
-> Unit)?으로 변경하고 내부 when 분기에서 SnackbarResult.ActionPerformed ->
onAction?.invoke()로 안전 호출을 사용하거나(또는 대신 action가 없는 오버로드
showPrezelSnackbar(message: String, ... onAction: null)를 추가) 변경 후 관련 호출부(모든
showPrezelSnackbar 호출 지점)를 찾아 nullable 시그니처에 맞게 업데이트하세요.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Prezel/app/build.gradle.kts`:
- Around line 63-64: 중복된 Gradle 의존성 선언을 제거하세요: 프로젝트에서 이미 선언된
implementation(projects.featureLoginApi)와
implementation(projects.featureLoginImpl) 중 해당 중복 라인들을 삭제해서 각 의존성이 한 번만 선언되도록
정리하면 됩니다; 대상 식별자는 projects.featureLoginApi 및 projects.featureLoginImpl 입니다.

In
`@Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/actions/area/ButtonAreaScope.kt`:
- Around line 113-116: validateButtonCount currently clears _buttons when size
== 2 which silently deletes existing buttons because callers
(MainButton/SubButton) still proceed to add the third; change
validateButtonCount to NOT clear the list and instead return a Boolean (e.g.,
canAddButton()) that returns false when _buttons.size >= 2, or alternatively
throw an exception—do not mutate _buttons inside validateButtonCount; then
update callers like MainButton and SubButton to call the new
canAddButton()/validate method and skip adding (/_buttons += {...}) when it
returns false (or allow the exception to propagate if you choose the fail-fast
option).

In
`@Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/util/ComposeMultipleEventCutter.kt`:
- Around line 15-30: ComposeMultipleEventCutter is a singleton that shares
lastEventTimeMs across the whole app so clicks on different buttons interfere;
change to per-component state by removing the global singleton usage and provide
a composable factory (e.g., rememberMultipleEventCutter) that keeps an
instance-local lastEventTimeMs via Compose's remember, and replace callers of
ComposeMultipleEventCutter.processEvent(...) with a returned lambda (e.g.,
(onClick: () -> Unit) -> Unit) that checks System.currentTimeMillis() against
its own lastEventTimeMs and updates it when invoking the event; keep the
debounce interval parameterizable (time: Long) and preserve the same invocation
semantics as processEvent.
- Around line 8-13: 현재 두 번째 clickOnce extension (fun <T> ((T) ->
Unit).clickOnce(value: T, time: Long = 200L))가 즉시 this(value)를 실행해 첫 번째
overload와 API가 일치하지 않습니다; clickOnce가 (T) -> Unit을 반환하도록 시그니처를 변경하고 내부에서
ComposeMultipleEventCutter.processEvent(time) { this(it) } 로 감싸진 람다를 반환하도록 수정하여
호출 시점이 지연되게 만드세요; 대상 심볼: clickOnce extension 함수와
ComposeMultipleEventCutter.processEvent를 찾아 반환형과 호출 위치만 조정하면 됩니다.

In
`@Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/util/NoRippleInteractionSource.kt`:
- Around line 9-13: The NoRippleInteractionSource implementation violates the
MutableInteractionSource contract: since emit(interaction) is a no-op and
interactions is emptyFlow(), tryEmit(interaction) must return false (it
currently returns true). Update the NoRippleInteractionSource so
tryEmit(interaction) returns false, keeping emit(interaction) as a no-op and
interactions as emptyFlow(), and ensure the change is applied to the
NoRippleInteractionSource class' emit, tryEmit, and interactions members to
reflect that interactions are not emitted.

In `@Prezel/feature/login/impl/build.gradle.kts`:
- Line 12: The Kakao SDK dependency and concrete KakaoLoginManager in
feature/login/impl violate our layer boundaries; move the SDK dependency
(implementation(libs.kakao.user)) and KakaoLoginManager into a new/existing core
auth module (e.g., core/auth) and expose a minimal authentication interface from
feature/login/api (e.g., AuthProvider or LoginRepository) that declares the
needed methods; update feature/login/impl to depend only on that interface and
wire the core/auth KakaoLoginManager implementation to the API interface via DI
or a provider binding.

In
`@Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/kakao/KakaoLoginResult.kt`:
- Around line 4-6: The Success data class in KakaoLoginResult currently exposes
the raw accessToken (Success), which risks token leakage; remove the accessToken
property from the public Success type and stop returning tokens to UI-facing
layers. Instead, handle and persist the token inside the Kakao login
implementation (e.g., in the class that calls KakaoLoginResult like
KakaoLoginManager or similar), store it in secure storage or pass it to the
internal session/token handler, and return a token-less result (e.g., Success
with no payload or a boolean/enum) from KakaoLoginResult so the UI only gets
success/failure.

In
`@Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/LoginScreen.kt`:
- Around line 70-74: The Kakao success payload is being discarded in the
LoginUiEffect.LaunchKakaoLogin handler; modify the handler to extract the
payload returned by kakaoLoginManager.login(context) and pass it to the
ViewModel instead of calling viewModel.onLoginSuccess() with no data. Update
LoginViewModel’s onLoginSuccess (or add a new method like
onKakaoLoginSuccess(tokenOrPayload)) to accept the token/payload, invoke the
authentication/use-case that exchanges/establishes the session, and only emit
NavigateToHome after that exchange succeeds; ensure failure paths still call
viewModel.onLoginFailure(...) with the original loginFailureMessage or error
details.
- Around line 61-65: LoginScreen currently creates a concrete KakaoLoginManager
with remember (KakaoLoginManager in LoginScreen), coupling UI to SDK; instead,
remove the direct instantiation and have the LoginViewModel (or a use-case)
expose a login intent/call that the composable invokes (e.g.,
viewModel.onKakaoLoginRequested()) and let the ViewModel depend on an injected
core interface (e.g., KakaoAuthRepository or KakaoLoginUseCase) which wraps
KakaoLoginManager; update LoginScreen to only emit intents and render uiState
(viewModel.uiState) and update tests to inject a fake implementation into the
ViewModel for success/failure scenarios.

In `@Prezel/settings.gradle.kts`:
- Line 27: 현재의 maven { url =
java.net.URI("https://devrepo.kakao.com/nexus/content/groups/public/") } 리포지토리
선언에 content 필터가 빠져 있어 범위가 너무 넓습니다; 해당 블록에 content { includeGroup("com.kakao") }
(또는 필요시 includeGroupByRegex("com\\.kakao.*")) 를 추가해 com.kakao.sdk.* 아티팩트만 허용하도록
제한하세요.

---

Nitpick comments:
In `@Prezel/app/build.gradle.kts`:
- Around line 9-15: Check for existence of the local.properties file before
trying to load it: guard the rootProject.file("local.properties") call used to
populate localProperties and throw or log a clear error if the file doesn't
exist (instead of letting load(...) raise FileNotFoundException), then continue
to read KAKAO_NATIVE_APP_KEY into kakaoNativeAppKey as before; update the error
message for missing KAKAO_NATIVE_APP_KEY to remain clear.

In
`@Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/snackbar/PrezelSnackbar.kt`:
- Around line 66-73: The conditional rendering is fine but the let block
redundantly references visuals.actionLabel; change the lambda to use the
implicit it parameter instead of re-reading visuals.actionLabel so the
PrezelButton call uses it for text (and keep ButtonType.GHOST, ButtonSize.SMALL
and onClick = { data.performAction() } unchanged), i.e. update the
visuals.actionLabel?.let { ... } block to reference it inside the PrezelButton
call.

In
`@Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/snackbar/SnackbarHost.kt`:
- Around line 24-48: 함수 SnackbarHostState.showPrezelSnackbar에서 현재 actionLabel:
String 및 onAction: () -> Unit의 non-null 시그니처가 PrezelSnackbarVisuals.actionLabel:
String?과 불일치하므로, toast(액션 없음) 스타일을 지원하려면 actionLabel을 String?으로, onAction를 (()
-> Unit)?으로 변경하고 내부 when 분기에서 SnackbarResult.ActionPerformed ->
onAction?.invoke()로 안전 호출을 사용하거나(또는 대신 action가 없는 오버로드
showPrezelSnackbar(message: String, ... onAction: null)를 추가) 변경 후 관련 호출부(모든
showPrezelSnackbar 호출 지점)를 찾아 nullable 시그니처에 맞게 업데이트하세요.

In
`@Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/util/ComposeMultipleEventCutter.kt`:
- Around line 25-29: 현재 ComposeMultipleEventCutter 내에서 이벤트 실행 여부와 상관없이
lastEventTimeMs를 항상 갱신해 빠른 연속 클릭 시 디바운스가 계속 연장되는 문제가 있습니다; if 검사로 이벤트를 실행하는
위치(조건문: if (now - lastEventTimeMs >= time) { event.invoke() }) 안으로
lastEventTimeMs = now 할당을 옮겨 이벤트가 실제로 실행될 때만 타임스탬프를 갱신하도록 변경하세요.

In
`@Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/kakao/KakaoLoginManager.kt`:
- Around line 10-29: The login function uses suspendCancellableCoroutine but
doesn't guard against resuming a cancelled continuation; modify the callback
lambda (the one assigned to callback in login) to check continuation.isActive
before calling continuation.resume (for Success/Failure) and ensure you register
continuation.invokeOnCancellation to clean up or cancel any ongoing SDK
operation; update both call sites UserApiClient.instance.loginWithKakaoTalk(...)
and UserApiClient.instance.loginWithKakaoAccount(...) to rely on this guarded
callback so the suspended coroutine won’t be resumed after cancellation.
- Around line 9-41: KakaoLoginManager is instantiated directly in UI which
hinders testing and DI; make it injectable via Hilt by converting it to accept
its dependencies in the constructor (e.g., UserApiClient or a small interface
like KakaoAuthClient) and annotate the class with `@Singleton` and an `@Inject`
constructor (or provide a `@Provides/`@Binds in a Hilt module if you prefer an
interface). Update usages (e.g., LoginScreen) to obtain KakaoLoginManager via
`@Inject` or hiltViewModel, and ensure methods login and logout continue to
delegate to the injected UserApiClient/KakaoAuthClient; add a Hilt module only
if you need to adapt the existing UserApiClient into a bindable implementation.

In
`@Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginViewModel.kt`:
- Around line 31-47: onLoginSuccess and onLoginFailure are public and can be
invoked repeatedly regardless of current state; add a guard or reduce visibility
to prevent duplicate handling: either change the functions' visibility to
internal or, inside onLoginSuccess/onLoginFailure, early-return unless the
current uiState indicates a login is in-progress (check
_uiState.value.isLoading), then set isLoading=false and send the ui effect.
Reference the functions onLoginSuccess/onLoginFailure, the _uiState state
property, _uiEffect, and the effects LoginUiEffect.NavigateToHome and
LoginUiEffect.ShowSnackbar when making the change.
- Around line 42-44: Replace the hardcoded default message in the LoginViewModel
emission of LoginUiEffect.ShowSnackbar with the string resource
feature_login_impl_kakao_failure; do this by removing the literal fallback ("카카오
로그인에 실패했습니다. 다시 시도해주세요.") and fetching the resource instead — either accept a
localized message from the UI caller, or inject/use a
ResourceProvider/StringProvider into LoginViewModel to call
getString(R.string.feature_login_impl_kakao_failure) when message is null, and
then pass that resource-derived text into LoginUiEffect.ShowSnackbar.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 64795d13-6f05-497f-9565-7075da7d6b4b

📥 Commits

Reviewing files that changed from the base of the PR and between 30a070a and 1fd4b48.

📒 Files selected for processing (21)
  • Prezel/app/build.gradle.kts
  • Prezel/app/src/main/AndroidManifest.xml
  • Prezel/app/src/main/java/com/team/prezel/PrezelApplication.kt
  • Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelNavigationBar.kt
  • Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/actions/area/ButtonAreaScope.kt
  • Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/snackbar/PrezelSnackbar.kt
  • Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/snackbar/SnackbarHost.kt
  • Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/util/ComposeMultipleEventCutter.kt
  • Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/util/NoRippleInteractionSource.kt
  • Prezel/feature/login/impl/build.gradle.kts
  • Prezel/feature/login/impl/src/main/AndroidManifest.xml
  • Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/LoginScreen.kt
  • Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/kakao/KakaoLoginManager.kt
  • Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/kakao/KakaoLoginResult.kt
  • Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginUiEffect.kt
  • Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginUiIntent.kt
  • Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginUiState.kt
  • Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginViewModel.kt
  • Prezel/feature/login/impl/src/main/res/values/strings.xml
  • Prezel/gradle/libs.versions.toml
  • Prezel/settings.gradle.kts

* refactor: 카카오 로그인 로직 core:data 모듈 이동 및 인터페이스화

- `feature:login:impl`에 위치하던 `KakaoLoginManager`를 `core:data` 모듈로 이동했습니다.
- `KakaoLoginManager`를 인터페이스로 정의하고, `KakaoLoginManagerImpl`에서 상세 로직을 구현하도록 변경했습니다.
- `KakaoLoginResult` sealed interface를 `core:data`로 이동했습니다.
- Hilt를 사용하여 `RepositoryModule`에서 `KakaoLoginManager` 의존성을 주입하도록 설정했습니다.

* refactor: LoginViewModel 내 카카오 로그인 비즈니스 로직 통합

- `LoginUiIntent`를 제거하고 `onClickLogin` 메서드로 직접 이벤트를 처리하도록 변경했습니다.
- `LoginScreen`에서 수행하던 카카오 로그인 호출 로직을 `LoginViewModel` 내부로 이동하여 뷰모델의 책임을 강화했습니다.
- `LoginUiEffect`에서 더 이상 사용되지 않는 `LaunchKakaoLogin`을 제거했습니다.
- 로그인 성공/실패 시의 상태 업데이트 및 Side Effect 처리 로직을 정리했습니다.

* build: 모듈 간 의존성 정리

- `core:data` 모듈에 `kakao.user` 라이브러리 의존성을 추가했습니다.
- `feature:login:impl`에서 `kakao.user` 의존성을 제거하고 `core:data`를 참조하도록 수정했습니다.
- `app` 모듈의 `build.gradle.kts`에서 불필요하게 명시되어 있던 `feature:login` 관련 의존성을 제거했습니다.

* style: LoginViewModel 및 LoginScreen 코드 스타일 정리

- `LoginViewModel` 및 `LoginScreen` 내 불필요한 공백을 제거하고 코드를 정렬했습니다.
- `LoginScreen`의 `onLogin` 콜백에서 Context와 실패 메시지를 ViewModel로 전달하도록 수정했습니다.
*   **ComposeMultipleEventCutter**: `clickOnce` 확장 함수가 값을 직접 받아 실행하던 방식에서, 중복 클릭 방지 로직이 적용된 람다 함수를 반환하도록 구조를 변경했습니다.
*   **NoRippleInteractionSource**: `tryEmit`의 반환값을 `true`에서 `false`로 수정하여 상호작용 이벤트가 실제로 방출되지 않음을 명확히 했습니다.
* refactor: KakaoLoginResult에서 액세스 토큰 제거 및 데이터 객체로 변경

`KakaoLoginResult.Success`에서 불필요한 `accessToken` 필드를 제거하고 `data object`로 전환했습니다.

*   `KakaoLoginResult.Success`를 `data class`에서 `data object`로 변경
*   `KakaoLoginManagerImpl`에서 로그인 성공 시 토큰을 전달하지 않고 `KakaoLoginResult.Success`만 반환하도록 수정
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@Prezel/core/data/src/main/java/com/team/prezel/core/data/auth/KakaoLoginManagerImpl.kt`:
- Around line 27-31: The current flow in KakaoLoginManagerImpl uses
UserApiClient.instance.isKakaoTalkLoginAvailable(context) and directly calls
loginWithKakaoTalk(..., callback) without falling back; update the logic inside
the method that calls UserApiClient.instance.loginWithKakaoTalk so that on
failure callbacks you inspect the error (ClientErrorCause.Cancelled,
NotSupported and other recoverable errors) and invoke
UserApiClient.instance.loginWithKakaoAccount(...) as a fallback; ensure the
original callback is forwarded with the final result and avoid double-calling
the success path by only calling the account login when the talk login error is
recoverable.

In
`@Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/LoginScreen.kt`:
- Around line 69-73: Replace hardcoded UI strings with string resources: change
the literal "확인" passed to snackbarHostState.showPrezelSnackbar and the literal
"카카오로 시작하기" used for the Kakao start button to use stringResource(R.string.*)
instead, and add corresponding entries (e.g., confirm and start_with_kakao) to
strings.xml (including translations as needed). Update the call sites in
LoginScreen.kt to call stringResource(...) for those labels so they match the
existing pattern and enable localization and easy copy/QA updates.
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 56c26288-fa77-4266-acb5-7dd459b1b0c6

📥 Commits

Reviewing files that changed from the base of the PR and between 1fd4b48 and a102eed.

📒 Files selected for processing (13)
  • Prezel/app/build.gradle.kts
  • Prezel/core/data/build.gradle.kts
  • Prezel/core/data/src/main/java/com/team/prezel/core/data/auth/KakaoLoginManager.kt
  • Prezel/core/data/src/main/java/com/team/prezel/core/data/auth/KakaoLoginManagerImpl.kt
  • Prezel/core/data/src/main/java/com/team/prezel/core/data/auth/KakaoLoginResult.kt
  • Prezel/core/data/src/main/java/com/team/prezel/core/data/di/RepositoryModule.kt
  • Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/util/ComposeMultipleEventCutter.kt
  • Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/util/NoRippleInteractionSource.kt
  • Prezel/feature/login/impl/build.gradle.kts
  • Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/LoginScreen.kt
  • Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginUiEffect.kt
  • Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginViewModel.kt
  • Prezel/settings.gradle.kts
✅ Files skipped from review due to trivial changes (4)
  • Prezel/core/data/build.gradle.kts
  • Prezel/feature/login/impl/build.gradle.kts
  • Prezel/core/data/src/main/java/com/team/prezel/core/data/auth/KakaoLoginResult.kt
  • Prezel/settings.gradle.kts
🚧 Files skipped from review as they are similar to previous changes (4)
  • Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginUiEffect.kt
  • Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/util/NoRippleInteractionSource.kt
  • Prezel/app/build.gradle.kts
  • Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/util/ComposeMultipleEventCutter.kt

`local.properties` 및 `buildConfig`에서 사용하는 키 명칭을 대문자 스네이크 케이스(Upper Snake Case)에서 소문자 도트 표기법(Dot notation)으로 변경하여 일관성을 높였습니다.

*   `app`: 카카오 네이티브 앱 키 참조명을 `KAKAO_NATIVE_APP_KEY`에서 `kakao.native.app.key`로 수정
*   `core:network`: 빌드 타입별 베이스 URL 키 생성 로직을 `${buildType.uppercase()}_BASE_URL`에서 `${buildType}.base.url` 형식으로 수정
* feat: 로그인 성공 시 화면 전환 애니메이션 및 딜레이 추가

로그인 성공 후 홈 화면으로 이동할 때 갑작스러운 UI 변화를 방지하기 위해 전환 로직을 개선했습니다.
* `LoginScreen`에서 홈 이동 시 `fadeOut` 애니메이션(120ms) 및 이에 맞춘 `delay`를 적용했습니다.
* 내비게이션 바가 나타날 때 앱 콘텐츠와 자연스럽게 어우러지도록 `NAV_TRANSITION_DURATION`(100ms) 지연 후 표시되도록 수정했습니다.

* refactor: LoginScreen 상태 관리 및 UI 로직 최적화

* `isLoading` 상태 대신 `showLoginButton` 플래그를 사용하여 화면 전환 시 버튼 노출 여부를 제어하도록 변경했습니다.
* `LoginFooter`의 `AnimatedVisibility`에 `fadeOut` 효과를 추가했습니다.
* `LoginViewModel`의 `uiState` 관찰 로직을 단순화하고 불필요한 `collectAsStateWithLifecycle` 참조를 제거했습니다.
* 프리뷰 코드에서 사용하지 않는 로딩 상태 케이스를 정리했습니다.

* fix: PrezelApp 내비게이션 바 표시 조건부 렌더링 수정

`PrezelApp`에서 `appState.shouldShowNavigationBar` 변경 시 `LaunchedEffect`를 통해 상태를 비동기적으로 업데이트하여 화면 전환 애니메이션과의 싱크를 맞췄습니다.
`LoginViewModel`에서 `StateFlow` 기반의 `LoginUiState`를 제거하고, 내부 변수를 사용하는 방식으로 상태 관리 로직을 단순화했습니다.

* `LoginUiState` 클래스 삭제
* `LoginViewModel` 내 `MutableStateFlow` 형태의 `_uiState`를 삭제하고 `isLoginInProgress` 플래그 변수로 대체
* 로그인 진행 상태에 따른 중복 호출 방지 및 상태 업데이트 로직 수정 (Flow 업데이트 대신 변수 값 할당)
* feat: 카카오 로그인 속도 제한(Rate Limit) 대응 추가

카카오 로그인 시도 횟수가 초과되었을 때의 상태를 정의하고 사용자에게 알림을 표시하도록 기능을 추가했습니다.

*   `KakaoLoginResult`: `RateLimited` 상태 추가
*   `LoginViewModel`: 로그인 실패 시나리오에 `rateLimitMessage` 처리 로직 추가
*   `strings.xml`: "로그인 시도가 너무 많습니다" 안내 문구 추가
*   `core:data`: `timber` 의존성 추가 및 `AuthError`(429) 발생 시 `RateLimited`로 변환하는 로직 구현

* refactor: 카카오 로그인 로직 고도화 및 로깅 추가

카카오톡 앱 로그인 실패 시 카카오 계정 로그인으로 자동 전환되는 로직을 개선하고, `Timber`를 통한 실행 로그를 추가했습니다.

*   카카오톡 로그인 중 사용자 취소(`ClientErrorCause.Cancelled`) 시 즉시 실패 처리
*   카카오톡 로그인 불가 혹은 오류 시 `loginWithKakaoAccount`로 fallback 처리
*   로그인 단계별(성공, 실패, 취소) 로그 기록 추가

* refactor: 앱 네비게이션 바 표시 로직 및 애니메이션 최적화

*   `PrezelApp`: 네비게이션 바 표시 여부를 결정할 때 사용하던 불필요한 `LaunchedEffect`와 `delay` 로직을 제거하고 `appState`를 직접 참조하도록 단순화했습니다.
*   `LoginScreen`: 로그인 버튼 영역의 `AnimatedVisibility` 종료 애니메이션(`exit`)을 `None`으로 변경하여 화면 전환 시 시각적 일관성을 개선했습니다.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 27, 2026

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 2 file(s) based on 1 unresolved review comment.

A stacked PR containing fixes has been created.

  • Stacked PR: #87
  • Files modified:
  • Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/LoginScreen.kt
  • Prezel/feature/login/impl/src/main/res/values/strings.xml

Time taken: 2m 45s

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/LoginScreen.kt (1)

161-169: 카카오 브랜드 색상을 상수로 추출하는 것을 고려해보세요.

Color(0xFFFEE500)은 카카오 공식 브랜드 색상입니다. 다른 곳에서도 재사용할 가능성이 있다면 디자인 시스템에 상수로 정의하는 것이 좋습니다.

예시
// PrezelColors 또는 별도 파일에 추가
object KakaoBrandColors {
    val Yellow = Color(0xFFFEE500)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/LoginScreen.kt`
around lines 161 - 169, Extract the hardcoded Kakao yellow Color(0xFFFEE500)
into a shared constant and replace its usage in the kakaoButtonConfig
declaration; create a new constant (e.g., KakaoBrandColors.Yellow or add to
PrezelColors) and update PrezelButtonDefaults.getDefault call in LoginScreen.kt
to use that constant instead of Color(0xFFFEE500) so the brand color can be
reused consistently across the codebase.
Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginViewModel.kt (1)

31-50: isLoginInProgress 플래그 리셋을 finally 블록으로 통합하세요.

각 분기마다 isLoginInProgress = false를 반복 설정하고 있어 유지보수 시 누락될 위험이 있습니다. try-finally 패턴으로 통합하면 코드가 간결해지고 예외 발생 시에도 플래그가 확실히 리셋됩니다.

리팩토링 제안
         viewModelScope.launch {
             isLoginInProgress = true
-
-            when (val result = kakaoLoginManager.login(context)) {
-                is KakaoLoginResult.Success -> {
-                    isLoginInProgress = false
-                    _uiEffect.send(LoginUiEffect.NavigateToHome)
-                }
-
-                is KakaoLoginResult.RateLimited -> {
-                    isLoginInProgress = false
-                    _uiEffect.send(LoginUiEffect.ShowSnackbar(rateLimitMessage))
-                }
-
-                is KakaoLoginResult.Failure -> {
-                    isLoginInProgress = false
-                    _uiEffect.send(LoginUiEffect.ShowSnackbar(failureMessage))
+            try {
+                when (val result = kakaoLoginManager.login(context)) {
+                    is KakaoLoginResult.Success -> {
+                        _uiEffect.send(LoginUiEffect.NavigateToHome)
+                    }
+                    is KakaoLoginResult.RateLimited -> {
+                        _uiEffect.send(LoginUiEffect.ShowSnackbar(rateLimitMessage))
+                    }
+                    is KakaoLoginResult.Failure -> {
+                        _uiEffect.send(LoginUiEffect.ShowSnackbar(failureMessage))
+                    }
                 }
+            } finally {
+                isLoginInProgress = false
             }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginViewModel.kt`
around lines 31 - 50, The isLoginInProgress flag is set to false in every when
branch which is error-prone; refactor the viewModelScope.launch block that calls
kakaoLoginManager.login(context) so you set isLoginInProgress = true before a
try, run the when (val result = kakaoLoginManager.login(context)) { ... } inside
the try, and move isLoginInProgress = false into a finally block to guarantee
reset on success, rate limit, failure or exception; keep existing handling for
KakaoLoginResult.Success/RateLimited/Failure and the
_uiEffect.send(LoginUiEffect.*) calls unchanged inside the when.
Prezel/core/data/src/main/java/com/team/prezel/core/data/auth/KakaoLoginManagerImpl.kt (1)

17-70: 코루틴 취소 시 SDK 콜백 정리가 누락되었습니다.

suspendCancellableCoroutine을 사용하지만 invokeOnCancellation이 등록되지 않았습니다. 코루틴이 취소되면(예: ViewModel이 cleared되면) Kakao SDK 콜백이 여전히 실행될 수 있으며, 이미 취소된 continuation에 resume을 시도하면 예외가 발생할 수 있습니다.

취소 처리 추가 제안
     override suspend fun login(context: Context): KakaoLoginResult =
         suspendCancellableCoroutine { continuation ->
+            continuation.invokeOnCancellation {
+                Timber.d("카카오 로그인이 취소되었습니다.")
+            }
+
             val accountLoginCallback: (OAuthToken?, Throwable?) -> Unit = { token, error ->
+                if (continuation.isActive) {
                     when {
                         error != null -> {
                             Timber.e(error, "카카오 계정 로그인에 실패했습니다.")
                             continuation.resume(error.toLoginResult())
                         }
                         // ... rest of the callback
                     }
+                }
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Prezel/core/data/src/main/java/com/team/prezel/core/data/auth/KakaoLoginManagerImpl.kt`
around lines 17 - 70, The coroutine can be cancelled while the Kakao SDK
callbacks still may fire; add cancellation handling in login: create a
cancellable flag (e.g., AtomicBoolean cancelled = false) and register
continuation.invokeOnCancellation { cancelled.set(true) } then guard every
continuation.resume call in accountLoginCallback and the inline
loginWithKakaoTalk callback with if (!cancelled.get()) to avoid resuming a
cancelled continuation; also, if the SDK exposes an explicit cancel/unregister
for login requests, call it from invokeOnCancellation. Ensure references to
accountLoginCallback, UserApiClient.instance.loginWithKakaoTalk,
UserApiClient.instance.loginWithKakaoAccount, and continuation are used so you
update the exact callbacks in login().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@Prezel/core/data/src/main/java/com/team/prezel/core/data/auth/KakaoLoginManagerImpl.kt`:
- Around line 17-70: The coroutine can be cancelled while the Kakao SDK
callbacks still may fire; add cancellation handling in login: create a
cancellable flag (e.g., AtomicBoolean cancelled = false) and register
continuation.invokeOnCancellation { cancelled.set(true) } then guard every
continuation.resume call in accountLoginCallback and the inline
loginWithKakaoTalk callback with if (!cancelled.get()) to avoid resuming a
cancelled continuation; also, if the SDK exposes an explicit cancel/unregister
for login requests, call it from invokeOnCancellation. Ensure references to
accountLoginCallback, UserApiClient.instance.loginWithKakaoTalk,
UserApiClient.instance.loginWithKakaoAccount, and continuation are used so you
update the exact callbacks in login().

In
`@Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/LoginScreen.kt`:
- Around line 161-169: Extract the hardcoded Kakao yellow Color(0xFFFEE500) into
a shared constant and replace its usage in the kakaoButtonConfig declaration;
create a new constant (e.g., KakaoBrandColors.Yellow or add to PrezelColors) and
update PrezelButtonDefaults.getDefault call in LoginScreen.kt to use that
constant instead of Color(0xFFFEE500) so the brand color can be reused
consistently across the codebase.

In
`@Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginViewModel.kt`:
- Around line 31-50: The isLoginInProgress flag is set to false in every when
branch which is error-prone; refactor the viewModelScope.launch block that calls
kakaoLoginManager.login(context) so you set isLoginInProgress = true before a
try, run the when (val result = kakaoLoginManager.login(context)) { ... } inside
the try, and move isLoginInProgress = false into a finally block to guarantee
reset on success, rate limit, failure or exception; keep existing handling for
KakaoLoginResult.Success/RateLimited/Failure and the
_uiEffect.send(LoginUiEffect.*) calls unchanged inside the when.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d1f18859-4700-4868-8420-db229e213f00

📥 Commits

Reviewing files that changed from the base of the PR and between a102eed and 64eebf9.

📒 Files selected for processing (10)
  • Prezel/app/build.gradle.kts
  • Prezel/app/src/main/java/com/team/prezel/ui/PrezelApp.kt
  • Prezel/core/data/build.gradle.kts
  • Prezel/core/data/src/main/java/com/team/prezel/core/data/auth/KakaoLoginManagerImpl.kt
  • Prezel/core/data/src/main/java/com/team/prezel/core/data/auth/KakaoLoginResult.kt
  • Prezel/core/network/build.gradle.kts
  • Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/LoginScreen.kt
  • Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginUiState.kt
  • Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginViewModel.kt
  • Prezel/feature/login/impl/src/main/res/values/strings.xml
💤 Files with no reviewable changes (1)
  • Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginUiState.kt
✅ Files skipped from review due to trivial changes (2)
  • Prezel/core/data/build.gradle.kts
  • Prezel/app/src/main/java/com/team/prezel/ui/PrezelApp.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • Prezel/feature/login/impl/src/main/res/values/strings.xml
  • Prezel/app/build.gradle.kts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginViewModel.kt (1)

30-51: isLoginInProgress 플래그 설정 위치 개선을 고려해 주세요.

현재 isLoginInProgress = truelaunch 블록 내부에서 설정되어, 연속 클릭 시 미세한 레이스 컨디션 가능성이 있습니다. Dispatchers.Main.immediate 덕분에 실제 문제가 될 가능성은 낮지만, launch 호출 전에 플래그를 설정하고 finally 블록에서 해제하면 더 안전합니다.

제안
 fun onClickLogin(
     context: Context,
     failureMessage: String,
     rateLimitMessage: String,
 ) {
     if (isLoginInProgress) return
+    isLoginInProgress = true

     viewModelScope.launch {
-        isLoginInProgress = true
-
-        when (val result = kakaoLoginManager.login(context)) {
-            is KakaoLoginResult.Success -> {
-                isLoginInProgress = false
-                _uiEffect.send(LoginUiEffect.NavigateToHome)
-            }
-
-            is KakaoLoginResult.RateLimited -> {
-                isLoginInProgress = false
-                _uiEffect.send(LoginUiEffect.ShowSnackbar(rateLimitMessage))
-            }
-
-            is KakaoLoginResult.Failure -> {
-                isLoginInProgress = false
-                _uiEffect.send(LoginUiEffect.ShowSnackbar(failureMessage))
-            }
+        try {
+            when (val result = kakaoLoginManager.login(context)) {
+                is KakaoLoginResult.Success -> _uiEffect.send(LoginUiEffect.NavigateToHome)
+                is KakaoLoginResult.RateLimited -> _uiEffect.send(LoginUiEffect.ShowSnackbar(rateLimitMessage))
+                is KakaoLoginResult.Failure -> _uiEffect.send(LoginUiEffect.ShowSnackbar(failureMessage))
+            }
+        } finally {
+            isLoginInProgress = false
         }
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginViewModel.kt`
around lines 30 - 51, Move the isLoginInProgress flag assignment out of the
coroutine body so it is set immediately before calling viewModelScope.launch (in
the LoginViewModel's login flow that calls kakaoLoginManager.login), and ensure
you clear the flag in a finally block inside the launched coroutine (after
handling KakaoLoginResult.Success/RateLimited/Failure) so the flag is always
reset even on exceptions; update references to isLoginInProgress accordingly to
avoid the tiny race window.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/LoginScreen.kt`:
- Around line 115-125: LoginFooter and SnackbarHost are both aligned
BottomCenter causing visual overlap when a snackbar appears; update the layout
so the SnackbarHost is offset above the footer (or give it bottom padding)
instead of sharing the exact same Alignment.BottomCenter. Locate the LoginFooter
usage and the SnackbarHost(hostState = snackbarHostState, snackbar = {
PrezelSnackbar(...) }) and change the SnackbarHost's Modifier to include an
appropriate bottom offset (e.g., padding(bottom = <footerHeight> dp) or use
WindowInsets/navigationBarsPadding to push the snackbar above the footer) so the
snackbar is rendered above the LoginFooter without overlap.

---

Nitpick comments:
In
`@Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginViewModel.kt`:
- Around line 30-51: Move the isLoginInProgress flag assignment out of the
coroutine body so it is set immediately before calling viewModelScope.launch (in
the LoginViewModel's login flow that calls kakaoLoginManager.login), and ensure
you clear the flag in a finally block inside the launched coroutine (after
handling KakaoLoginResult.Success/RateLimited/Failure) so the flag is always
reset even on exceptions; update references to isLoginInProgress accordingly to
avoid the tiny race window.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fcc08d12-211d-4c05-81cf-cf7e9c9f306c

📥 Commits

Reviewing files that changed from the base of the PR and between 64eebf9 and 000df44.

📒 Files selected for processing (4)
  • Prezel/core/data/src/main/java/com/team/prezel/core/data/auth/KakaoLoginManagerImpl.kt
  • Prezel/core/network/build.gradle.kts
  • Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/LoginScreen.kt
  • Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginViewModel.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • Prezel/core/network/build.gradle.kts

* refactor: LoginScreen 내 SnackbarHostState 전역 관리 적용

`LocalSnackbarHostState` CompositionLocal을 사용하여 `LoginScreen`에서 직접 관리하던 `SnackbarHostState`를 전역 상태로 전환했습니다.

*   `LoginScreen`에서 로컬 `remember { SnackbarHostState() }` 제거 및 `LocalSnackbarHostState.current` 사용
*   `LoginContent`에서 불필요한 `snackbarHostState` 파라미터 및 `SnackbarHost` 컴포저블 제거
*   Preview 코드에 `CompositionLocalProvider`를 적용하여 `LocalSnackbarHostState` 주입

* refactor: LoginViewModel 코드 스타일 및 로직 최적화

*   클래스 선언 및 생성자 주입 부분의 들여쓰기 스타일을 수정했습니다.
*   `onClickLogin` 내 `when` 식에서 불필요한 변수 할당(`val result = ...`)을 제거했습니다.

* fix: ButtonAreaScope 내 버튼 개수 검증 로직 수정

`validateButtonCount` 함수에서 버튼이 2개일 때 리스트를 초기화하던 잘못된 로직을 삭제하고, 2개 미만인지 확인하는 `require` 구문만 남겨 검증 로직을 올바르게 수정했습니다.
* feat: LoginUiIntent 및 LoginUiState 정의

로그인 화면의 상태 관리와 사용자 액션 처리를 위해 MVI 패턴의 기초가 되는 Intent와 State 클래스를 추가했습니다.

*   `LoginUiIntent`: `OnClickLogin` 액션 정의 (Context 및 에러 메시지 포함)
*   `LoginUiState`: 로딩 상태(`isLoading`)를 관리하도록 정의

* refactor: LoginViewModel MVI 패턴 적용 및 로직 개선

`LoginViewModel`의 상태 관리 방식을 개선하고 사용자 입력을 `onIntent`를 통해 처리하도록 리팩토링했습니다.

*   `isLoginInProgress`와 `isNavigatingToHome`으로 분산되어 있던 상태를 `isLoading`으로 통합
*   `onClickLogin` 직접 호출 방식에서 `LoginUiIntent`를 통한 처리 방식으로 변경
*   로그인 성공 시 UI State를 통한 네비게이션 제어 대신 `LoginUiEffect`를 통한 단발성 이벤트 처리로 통일

* refactor: LoginScreen 상태 연동 및 UI 로직 개선

ViewModel의 `uiState`를 화면에 반영하고, 로그인 진행 중 버튼 비활성화 로직을 추가했습니다.

*   `collectAsStateWithLifecycle`을 사용하여 `uiState` 구독
*   `LoginFooter` 및 `LoginScreen`에 `isLoginEnabled` 파라미터를 추가하여 로딩 중 버튼 중복 클릭 방지
*   ViewModel의 `onIntent`를 호출하여 로그인 로직을 실행하도록 수정
*   Preview 코드에 신규 파라미터 대응 및 구조 업데이트
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/LoginScreen.kt (1)

79-82: ⚠️ Potential issue | 🟡 Minor

사용자 노출 문구는 리소스로 올려 주세요.

"확인""카카오로 시작하기"가 아직 하드코딩돼 있습니다. 이 파일은 이미 stringResource(...)를 쓰고 있으니 동일하게 리소스로 빼 두는 편이 번역/QA 대응에 안전합니다.

Also applies to: 197-200

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/LoginScreen.kt`
around lines 79 - 82, Replace hardcoded user-facing strings in the Snackbar and
button calls with string resources: find the
snackbarHostState.showPrezelSnackbar(...) invocation(s) (including the instance
using effect.message and actionLabel = "확인") and the UI element that uses "카카오로
시작하기", and swap those literal strings to use stringResource(R.string.xxx)
entries (create new string resources like confirm_label and kakao_start_label in
strings.xml). Keep effect.message as-is if it’s dynamic, but ensure any static
labels passed (actionLabel, button text) use stringResource(...) consistent with
other usages in LoginScreen.kt.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginUiIntent.kt`:
- Around line 6-10: The OnClickLogin intent currently carries an Android Context
which can leak Activity instances; remove the Context parameter from the data
class OnClickLogin (keep failureMessage and rateLimitMessage) so LoginUiIntent
is a pure event, and move any Context-requiring call (e.g.,
KakaoLoginManager.login(...)) out of LoginViewModel into the lifecycle owner
(Activity/Fragment) that handles the UI click and invokes KakaoLoginManager with
the proper Context; update places that create/dispatch OnClickLogin to stop
passing Context and instead have the UI layer observe the intent or ViewModel
event and call KakaoLoginManager.login(...) itself.

In
`@Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginViewModel.kt`:
- Around line 38-56: The login coroutine in LoginViewModel currently updates
_uiState.isLoading but if kakaoLoginManager.login(...) throws an exception
isLoading never gets reset; wrap the call in a try/catch/finally inside
viewModelScope.launch so that in finally you always set _uiState.update {
it.copy(isLoading = false) }, and in catch convert the exception into a failure
UI effect by sending _uiEffect.send(LoginUiEffect.ShowSnackbar(<use
intent.failureMessage or exception.message>)) so all exception paths reset
loading and show a failure snackbar.

---

Duplicate comments:
In
`@Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/LoginScreen.kt`:
- Around line 79-82: Replace hardcoded user-facing strings in the Snackbar and
button calls with string resources: find the
snackbarHostState.showPrezelSnackbar(...) invocation(s) (including the instance
using effect.message and actionLabel = "확인") and the UI element that uses "카카오로
시작하기", and swap those literal strings to use stringResource(R.string.xxx)
entries (create new string resources like confirm_label and kakao_start_label in
strings.xml). Keep effect.message as-is if it’s dynamic, but ensure any static
labels passed (actionLabel, button text) use stringResource(...) consistent with
other usages in LoginScreen.kt.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1374c88e-1588-4340-92f7-ad731dcfe424

📥 Commits

Reviewing files that changed from the base of the PR and between 000df44 and 6c6177f.

📒 Files selected for processing (4)
  • Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/LoginScreen.kt
  • Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginUiIntent.kt
  • Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginUiState.kt
  • Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginViewModel.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginUiState.kt

HamBeomJoon and others added 16 commits March 29, 2026 12:45
카카오 로그인 관련 기능을 `core:data` 모듈에서 신규 생성한 `core:auth` 모듈로 분리하여 모듈화 구조를 개선했습니다.

* **신규 모듈 생성 및 설정**:
    * `core:auth` 모듈을 추가하고 `settings.gradle.kts`에 등록했습니다.
    * `app` 모듈에 집중되어 있던 `KAKAO_NATIVE_APP_KEY` 설정 및 빌드 로직을 `core:auth` 모듈로 이동했습니다.
    * `AndroidManifest.xml` 내 카카오 인증 관련 `AuthCodeHandlerActivity` 설정을 `core:auth` 모듈로 이관했습니다.

* **인증 로직 이관**:
    * `KakaoLoginManager`, `KakaoLoginManagerImpl`, `KakaoLoginResult` 클래스를 `core:data.auth`에서 `core:auth` 패키지로 이동했습니다.
    * `KakaoAuthInitializer`를 추가하여 `Application` 클래스에서 수행하던 SDK 초기화 로직을 캡슐화했습니다.
    * Hilt 의존성 주입을 위한 `AuthModule`을 추가하고 기존 `RepositoryModule`의 바인딩을 정리했습니다.

* **의존성 및 참조 수정**:
    * `app`, `feature:login:impl` 모듈에서 `core:data` 대신 `core:auth`를 참조하도록 의존성을 수정했습니다.
    * `LoginViewModel` 등 관련 코드의 import 경로를 새 패키지에 맞게 업데이트했습니다.
    * `core:data` 및 `app` 모듈의 `build.gradle.kts`에서 불필요해진 카카오 SDK 및 Timber 의존성을 제거했습니다.
* refactor: LoginViewModel 내 KakaoLoginManager 의존성 제거 및 로직 이동

`LoginViewModel`에서 직접 수행하던 카카오 로그인 로직을 `LoginScreen`으로 이동하고, ViewModel은 상태 관리와 UI Effect 전달에 집중하도록 개선했습니다.

*   `LoginViewModel`: `KakaoLoginManager` 의존성 제거 및 `LaunchKakaoLogin` Effect 추가
*   `LoginUiIntent`: `OnClickLogin`에서 불필요한 `Context` 및 메시지 파라미터 제거, `OnLoginSuccess`, `OnLoginFailure` 인텐트 추가
*   `LoginUiEffect`: `LaunchKakaoLogin` 추가 및 `NavigateToHome` 처리 로직 단순화
*   `reduce` 확장 함수를 추가하여 `LoginUiState` 업데이트 로직 정형화

* refactor: LoginScreen 내 로그인 로직 구현 및 네비게이션 연동

`LoginScreen`에서 `KakaoLoginManager`를 직접 호출하여 로그인을 수행하고, 결과에 따라 ViewModel에 인텐트를 전달하도록 수정했습니다.

*   `LoginScreen`: `KakaoLoginManager` 파라미터 추가 및 `LaunchedEffect` 내에서 `LaunchKakaoLogin` Effect 처리 로직 구현
*   로그인 성공/실패 시 `viewModel.onIntent`를 통해 상태 업데이트 요청
*   `isNavigatingToHome` 상태에 따른 화면 전환 및 딜레이 로직 개선

* build: LoginEntryBuilder 의존성 주입 수정

`LoginEntryBuilder` 및 `FeatureLoginModule`에서 `KakaoLoginManager`를 주입받아 `LoginScreen`으로 전달하도록 수정했습니다.

*   `featureLoginEntryBuilder`: `KakaoLoginManager` 파라미터 추가 및 `LoginScreen` 호출부에 전달
*   `FeatureLoginModule`: `provideFeatureLoginEntryBuilder`에서 Hilt를 통해 `KakaoLoginManager` 주입 설정
* refactor: LoginScreen 내 Side Effect 처리 로직 분리

`LoginScreen` 컴포저블의 가독성을 높이기 위해 UI 효과(Effect)를 처리하는 로직을 별도의 `HandleLoginEffects` 컴포저블로 추출했습니다.

*   `HandleLoginEffects` 추가: 카카오 로그인 실행, 홈 화면 이동, 스낵바 표시 등 비즈니스 로직 수반 효과 분리
*   `LaunchedEffect` 키 수정: `Unit`에서 `uiEffect` 흐름(Flow)으로 변경하여 안정성 강화
*   `LoginScreen` 구조 개선: 상태 관리 로직과 UI 렌더링 부분을 명확히 구분

* feat: 로그인 관련 스낵바 확인 문구 리소스 추가

스낵바에서 사용되는 "확인" 버튼 텍스트를 하드코딩 대신 문자열 리소스로 관리하도록 수정했습니다.

*   `strings.xml`에 `feature_login_impl_snackbar_confirm` 리소스 추가
*   `showPrezelSnackbar` 호출 시 해당 리소스를 `actionLabel`로 사용하도록 반영
* refactor: `LoginScreen` 내 카카오 로그인 로직 분리 및 최적화

`LoginScreen` 컴포저블 내에 인라인으로 작성되어 있던 카카오 로그인 실행 및 결과 처리 로직을 별도의 함수로 추출하여 가독성을 개선했습니다.

*   `loginWithKakao`: `KakaoLoginManager`를 호출하고 결과를 `LoginUiIntent`로 변환하는 suspend 함수 추가
*   `toLoginIntent`: `KakaoLoginResult` 상태를 `LoginUiIntent`의 성공/실패 상태로 매핑하는 확장 함수 추가
*   `LoginScreen` 내 `uiEffect` 수집 로직에서 위 함수들을 사용하도록 변경하여 코드 간소화
`local.properties`에서 관리하는 Kakao Native App Key를 빌드 타입(debug, release)에 따라 개별적으로 설정할 수 있도록 개선하였습니다.

* `build.gradle.kts` 내 `setKakaoNativeAppKey` 확장 함수를 추가하여 중복 로직을 제거했습니다.
* `debug` 및 `release` 빌드 타입에서 각각 `${buildType}.kakao.native.app.key` 프로퍼티를 참조하도록 수정했습니다.
* `buildConfigField` 및 `manifestPlaceholders`에 빌드 타입별 키 값이 적용되도록 구성했습니다.

Co-authored-by: moondev03 <moondev03@gmail.com>
- 기존의 카카오 전용 인증 로직을 범용적인 `AuthManager` 및 `AuthClient` 구조로 추상화하여 확장성을 높였습니다.

Co-authored-by: moondev03 <moondev03@gmail.com>
Co-authored-by: moondev03 <moondev03@gmail.com>
- `SplashScreen`의 로직을 간소화하고, 로고 이미지의 레이아웃 방식을 개선하였습니다.

Co-authored-by: moondev03 <moondev03@gmail.com>
- `showPrezelSnackbar` 확장 함수를 더 유연하게 사용할 수 있도록 파라미터 구조를 개선했습니다.

Co-authored-by: moondev03 <moondev03@gmail.com>
- `PrezelApplication`에서 특정 플랫폼(Kakao)에 종속된 초기화 클래스 명칭을 더 범용적인 `AuthInitializer`로 변경하였습니다.

Co-authored-by: moondev03 <moondev03@gmail.com>
Co-authored-by: moondev03 <moondev03@gmail.com>
`LoginScreen.kt`에서 사용되지 않는 상수 `LOGIN_EXIT_DURATION`을 삭제하였습니다.

Co-authored-by: moondev03 <moondev03@gmail.com>
- 로그인 과정에서 발생하는 예외 상황에 대한 사용자 메시지를 더 부드러운 어조로 개선하고, 취소 케이스를 분리하였습니다.

Co-authored-by: moondev03 <moondev03@gmail.com>
* refactor: `LoginViewModel` 내 로그인 클릭 핸들링 로직 수정

- `LoginUiIntent.OnClickLogin`에서 `AuthProvider` 파라미터를 제거했습니다.
- 로그인 버튼 클릭 시 실제 인증 프로세스를 시작하는 대신, `LoginUiEffect.NavigateToHome`을 전송하여 즉시 홈 화면으로 이동하도록 임시 수정했습니다.
- 불필요한 `AuthProvider` 임포트 문을 제거했습니다.
@HamBeomJoon HamBeomJoon merged commit 116867d into develop Mar 30, 2026
2 checks passed
@HamBeomJoon HamBeomJoon deleted the #79-kakao-login branch March 30, 2026 04:29
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.

카카오 로그인 연동 구조 설계 및 초기 구현

2 participants