feat: 로그아웃, 회원탈퇴 api연결 - #61
Conversation
Gemini AI 코드리뷰Gemini API 응답을 읽지 못했습니다. {
"error": {
"code": 503,
"message": "This model is currently experiencing high demand. Spikes in demand are usually temporary. Please try again later.",
"status": "UNAVAILABLE"
}
} |
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthrough온보딩 입력과 제출 API를 추가했습니다. 토큰 저장소와 인증 만료 처리를 변경했습니다. 로그아웃·회원 탈퇴와 Kakao 로그아웃을 연결했습니다. 온보딩과 마이페이지의 로딩, 메시지, 로그인 이동 처리를 갱신했습니다. Changes온보딩 및 인증 흐름
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
Suggested labels: Sequence Diagram(s)sequenceDiagram
participant TokenAuthenticator
participant AuthSessionManager
participant AppViewModel
participant AppScreen
TokenAuthenticator->>AuthSessionManager: notifySessionExpired()
AuthSessionManager-->>AppViewModel: sessionExpired
AppViewModel-->>AppScreen: NavigateToLogin
AppScreen->>AppScreen: replace back stack with LoginNavKey
sequenceDiagram
participant Onboarding5Screen
participant OnboardingViewModel
participant OnboardingRepositoryImpl
participant MyPageApi
participant AICharacterApi
Onboarding5Screen->>OnboardingViewModel: submitOnboarding(preferTime)
OnboardingViewModel->>OnboardingRepositoryImpl: submit onboarding inputs
OnboardingRepositoryImpl->>MyPageApi: updateMember(request)
OnboardingRepositoryImpl->>AICharacterApi: createCharacter(request)
AICharacterApi-->>OnboardingRepositoryImpl: ApiResponse
OnboardingRepositoryImpl-->>OnboardingViewModel: Result<Unit>
🚥 Pre-merge checks | ✅ 2 | ❌ 5❌ Failed checks (5 warnings)
✅ Passed checks (2 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (2)
feature/mypage/impl/src/main/java/kr/co/call/impl/screen/MyPageScreen.kt (1)
57-57: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win사이드 이펙트 수집을
LaunchedEffect로 범위 지정하세요.현재
collectSideEffect를 Composable 본문에서 직접 호출합니다. 프로젝트 규칙에 맞게LaunchedEffect내부에서 ViewModel의 side-effect Flow를collect하세요.collectAsState()는 이미 lifecycle-aware 수집을 제공하므로 변경하지 마세요.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@feature/mypage/impl/src/main/java/kr/co/call/impl/screen/MyPageScreen.kt` at line 57, MyPageScreen의 collectSideEffect 호출을 Composable 본문에서 제거하고 LaunchedEffect 범위 안에서 ViewModel의 side-effect Flow를 수집하도록 변경하세요. 기존 collectAsState() 호출은 lifecycle-aware 수집을 위해 그대로 유지하세요.Sources: Path instructions, Learnings
feature/mypage/impl/src/main/java/kr/co/call/impl/viewmodel/MyPageViewModel.kt (1)
70-76: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift로그아웃과 탈퇴를 UseCase를 통해 호출하세요.
MyPageViewModel이MyPageRepository를 직접 호출합니다. 새 인증 흐름은 중복 방지, 상태 전이, 오류 처리, Kakao 로그아웃을 함께 조정합니다.
LogoutUseCase와DeleteAccountUseCase를core/domain에 두고 저장소 작업을 위임하세요. ViewModel은 UI 상태와 side effect 조정만 담당해야 합니다.As per path instructions,
**/*ViewModel.kt의 비즈니스 로직은core:domainUseCase 호출로 위임해야 합니다.Also applies to: 98-104
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@feature/mypage/impl/src/main/java/kr/co/call/impl/viewmodel/MyPageViewModel.kt` around lines 70 - 76, MyPageViewModel의 로그아웃 및 회원 탈퇴 비즈니스 로직을 직접 MyPageRepository에서 호출하지 않도록 변경하세요. core:domain의 LogoutUseCase와 DeleteAccountUseCase를 추가해 저장소 작업, Kakao 로그아웃, 상태 전이 및 오류 처리를 위임하고, MyPageViewModel은 해당 UseCase를 호출한 뒤 UI 상태와 MyPageSideEffect만 조정하도록 수정하세요.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@core/data/src/main/java/kr/co/call/data/repositoryImpl/LoginRepositoryImpl.kt`:
- Around line 50-55: Update the login flow around
accountStateDataStore.shouldForceOnboarding() and saveTokens() so the account
state is read successfully before persisting tokens. Move saveTokens() after all
required reads complete, while preserving the existing LoginToken
needsOnboarding calculation and ensuring a read failure returns login failure
without leaving authentication tokens stored.
In
`@core/data/src/main/java/kr/co/call/data/repositoryImpl/MyPageRepositoryImpl.kt`:
- Around line 43-51: In the deleteAccount function, separate the withdrawal
completion state management from token cleanup to handle partial failures. After
myPageApi.deleteAccount() succeeds, call accountStateDataStore.markWithdrawn()
to record the withdrawal state regardless of subsequent token deletion. Move
tokenDataStore.clearTokens() to execute as an independent non-blocking operation
that catches and logs its own failures without propagating them to the overall
result, since token deletion should not prevent the withdrawal from being marked
complete or cancel the deletion operation.
In
`@core/data/src/main/java/kr/co/call/data/repositoryImpl/OnboardingRepositoryImpl.kt`:
- Around line 26-67: Update submitOnboarding so the member update and character
creation are atomic, or make createCharacter idempotent by adding an idempotency
key or upsert contract. Ensure retries after updateMember succeeds do not create
duplicate characters and can safely resume the onboarding flow before
clearForceOnboarding() is called.
In
`@core/network/src/main/java/kr/co/call/network/interceptor/AuthInterceptor.kt`:
- Around line 26-28: Remove the runBlocking { tokenDataStore.getAccessToken() }
call from the AuthInterceptor fallback logic. The interceptor should only read
from memory cache using getCachedAccessToken() and avoid any synchronous
DataStore I/O operations that would block the OkHttp request thread. Instead,
move DataStore reads and token refresh operations to a dedicated token refresh
path that executes before requests are made, or implement explicit timeout and
failure policies at a higher level outside the interceptor itself.
In
`@feature/mypage/impl/src/main/java/kr/co/call/impl/viewmodel/MyPageViewModel.kt`:
- Around line 72-76: MyPageViewModel의 KakaoLogoutManager.logout() 실패를 로그만 남긴 채
NavigateToLogin으로 진행하지 않도록 수정하세요. 로그아웃 실패 시 재시도 가능한 사용자 오류로 처리해 로그인 화면 이동을
보류하거나, 이동 전에 카카오 SDK 세션을 별도로 종료해 잔존 세션이 자동 로그인을 유발하지 않도록 하세요.
In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TemporaryProfileImageData.kt`:
- Around line 8-13: Update the femaleImages definitions alongside the maleImages
list in the temporary profile image data so every selectable female
ProfileImageOption receives a valid imageUrl instead of an empty string. Reuse
the appropriate existing profile image URL constant or provide the corresponding
female image URL while preserving the existing option IDs and list structure.
In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt`:
- Around line 183-190: PR 설명에 화면 변경 증빙을 추가하세요. Onboarding1Screen.kt 183-190에서는
회원 직업 및 프로필 선택 동작을, Onboarding2Screen.kt 184-191에서는 AI 직업 선택과 다음 단계 활성화를,
Onboarding6Screen.kt 98-100에서는 제출 중 클릭 차단 동작을 보여주는 스크린샷 또는 화면 녹화를 각각 첨부하세요.
In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.kt`:
- Around line 184-191: Update the job-selection flow around selectedJob and
CharacterJob so the displayed options exactly match CharacterJob.label values.
Either replace the "기타" option with the existing "무직" label or add a
corresponding OTHER("기타") enum entry, ensuring selecting the option assigns a
non-null CharacterJob and allows canMoveNext to proceed.
In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingViewModel.kt`:
- Around line 118-205: Extract the validation, OnboardingSubmission
construction, trait priority assignment, and
onboardingRepository.submitOnboarding call from submitOnboarding into a
core-domain SubmitOnboardingUseCase. Update the ViewModel’s submitOnboarding
method to pass the required state inputs and call the use case, retaining only
UI state handling through reduce and postSideEffect, including loading, success,
cancellation, and error presentation.
---
Nitpick comments:
In `@feature/mypage/impl/src/main/java/kr/co/call/impl/screen/MyPageScreen.kt`:
- Line 57: MyPageScreen의 collectSideEffect 호출을 Composable 본문에서 제거하고
LaunchedEffect 범위 안에서 ViewModel의 side-effect Flow를 수집하도록 변경하세요. 기존
collectAsState() 호출은 lifecycle-aware 수집을 위해 그대로 유지하세요.
In
`@feature/mypage/impl/src/main/java/kr/co/call/impl/viewmodel/MyPageViewModel.kt`:
- Around line 70-76: MyPageViewModel의 로그아웃 및 회원 탈퇴 비즈니스 로직을 직접
MyPageRepository에서 호출하지 않도록 변경하세요. core:domain의 LogoutUseCase와
DeleteAccountUseCase를 추가해 저장소 작업, Kakao 로그아웃, 상태 전이 및 오류 처리를 위임하고,
MyPageViewModel은 해당 UseCase를 호출한 뒤 UI 상태와 MyPageSideEffect만 조정하도록 수정하세요.
🪄 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: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 336f2df7-334b-4a7d-87b8-a6797ad64073
📒 Files selected for processing (35)
app/src/main/java/kr/co/call/callfromai/AppScreen.ktcore/data/src/main/java/kr/co/call/data/di/RepositoryModule.ktcore/data/src/main/java/kr/co/call/data/repositoryImpl/LoginRepositoryImpl.ktcore/data/src/main/java/kr/co/call/data/repositoryImpl/MyPageRepositoryImpl.ktcore/data/src/main/java/kr/co/call/data/repositoryImpl/OnboardingRepositoryImpl.ktcore/datastore/src/main/java/kr/co/call/datastore/AccountStateDataStore.ktcore/domain/src/main/java/kr/co/call/domain/model/onboarding/OnboardingSubmission.ktcore/domain/src/main/java/kr/co/call/domain/repository/MyPageRepository.ktcore/domain/src/main/java/kr/co/call/domain/repository/OnboardingRepository.ktcore/network/build.gradle.ktscore/network/src/main/java/kr/co/call/network/api/AICharacterApi.ktcore/network/src/main/java/kr/co/call/network/api/MyPageApi.ktcore/network/src/main/java/kr/co/call/network/di/NetworkModule.ktcore/network/src/main/java/kr/co/call/network/dto/onboarding/CreateCharacterRequestDto.ktcore/network/src/main/java/kr/co/call/network/dto/onboarding/UpdateMemberRequestDto.ktcore/network/src/main/java/kr/co/call/network/interceptor/AuthInterceptor.ktcore/network/src/main/java/kr/co/call/network/util/SafeApiCall.ktfeature/mypage/impl/build.gradle.ktsfeature/mypage/impl/src/main/java/kr/co/call/impl/auth/KakaoLogoutManager.ktfeature/mypage/impl/src/main/java/kr/co/call/impl/entry/MyPageEntryBuilder.ktfeature/mypage/impl/src/main/java/kr/co/call/impl/screen/MyPageScreen.ktfeature/mypage/impl/src/main/java/kr/co/call/impl/viewmodel/MyPageSideEffect.ktfeature/mypage/impl/src/main/java/kr/co/call/impl/viewmodel/MyPageState.ktfeature/mypage/impl/src/main/java/kr/co/call/impl/viewmodel/MyPageViewModel.ktfeature/onboarding/impl/src/main/java/kr/co/call/impl/component/TemporaryProfileImageData.ktfeature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.ktfeature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.ktfeature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.ktfeature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding6Screen.ktfeature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingSideEffect.ktfeature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingUiState.ktfeature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingViewModel.ktfeature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/model/Job.ktfeature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/state/Onboarding1State.ktfeature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/state/Onboarding2State.kt
| val forceOnboarding=accountStateDataStore.shouldForceOnboarding() | ||
|
|
||
| LoginToken( | ||
| accessToken = result.accessToken, | ||
| refreshToken = result.refreshToken, | ||
| needsOnboarding = result.needsOnboarding, | ||
| needsOnboarding = result.needsOnboarding || forceOnboarding, |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate file =="
git ls-files | rg '(^|/)LoginRepositoryImpl\.kt$|AccountStateDataStore'
echo "== inspect LoginRepositoryImpl outline/size =="
FILE="core/data/src/main/java/kr/co/call/data/repositoryImpl/LoginRepositoryImpl.kt"
if [ -f "$FILE" ]; then
wc -l "$FILE"
ast-grep outline "$FILE" --view compact || true
echo "== relevant lines =="
cat -n "$FILE" | sed -n '1,120p'
fi
echo "== inspect AccountStateDataStore candidates =="
for f in $(git ls-files | rg 'AccountStateDataStore|accountStateDataStore'); do
echo "--- $f"
wc -l "$f"
ast-grep outline "$f" --view compact || true
cat -n "$f" | sed -n '1,180p'
done
echo "== search loginWithKakao/saveTokens usages =="
rg -n "loginWithKakao|saveTokens|shouldForceOnboarding|save.*Token|TokenRepository|needsOnboarding" core/data core/domain || trueRepository: call-from-ai/CallFromAi_Android
Length of output: 5550
토큰 저장 전에 계정 상태를 읽으세요.
shouldForceOnboarding() 실패 시 runCatching은 로그인 실패를 반환하지만, Line 45의 saveTokens()는 이미 성공합니다. 모든 읽기가 성공한 뒤에는 저장하도록 순서를 바꾸어 실패 시 인증 토큰이 남지 않게 하세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@core/data/src/main/java/kr/co/call/data/repositoryImpl/LoginRepositoryImpl.kt`
around lines 50 - 55, Update the login flow around
accountStateDataStore.shouldForceOnboarding() and saveTokens() so the account
state is read successfully before persisting tokens. Move saveTokens() after all
required reads complete, while preserving the existing LoginToken
needsOnboarding calculation and ensuring a read failure returns login failure
without leaving authentication tokens stored.
| override suspend fun deleteAccount(): Result<Unit> = | ||
| runRepositoryCatching { | ||
| //토큰이 남아 있을 때 탈퇴 api를 먼저 호출 | ||
| safeApiCallUnit(errorResponseParser){ | ||
| myPageApi.deleteAccount() | ||
| } | ||
| //서버 탈퇴 성공 후 기기의 토큰 삭제 | ||
| tokenDataStore.clearTokens() | ||
| accountStateDataStore.markWithdrawn() |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# DataStore 예외 전파와 탈퇴 플래그 소비 위치를 확인합니다.
fd -t f -a '^(TokenDataStore|AccountStateDataStore|LoginRepositoryImpl)\.kt$' . |
while IFS= read -r file; do
echo "===== $file ====="
rg -n -C 8 'clearTokens|markWithdrawn|withdrawn|onboarding|edit\(' "$file"
doneRepository: call-from-ai/CallFromAi_Android
Length of output: 278
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Changed/stat:"
git diff --stat || true
echo
echo "Find relevant files:"
fd -t f -a 'MyPageRepositoryImpl\.kt|TokenDataStore\.kt|AccountStateDataStore\.kt|LoginRepositoryImpl\.kt|MyPageRepository\.kt' . || true
echo
echo "Search symbols:"
rg -n 'deleteAccount|clearTokens|markWithdrawn|withdrawn|onboarding|runRepositoryCatching|safeApiCallUnit' .Repository: call-from-ai/CallFromAi_Android
Length of output: 11637
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "===== MyPageRepositoryImpl.kt ====="
cat -n core/data/src/main/java/kr/co/call/data/repositoryImpl/MyPageRepositoryImpl.kt | sed -n '1,90p'
echo
echo "===== safeApiCall utilities ====="
cat -n core/network/src/main/java/kr/co/call/network/util/SafeApiCall.kt | sed -n '1,140p'
cat -n core/data/src/main/java/kr/co/call/data/util/SafeApiResult.kt | sed -n '1,120p'
echo
echo "===== DataStore implementations ====="
cat -n core/datastore/src/main/java/kr/co/call/datastore/TokenDataStore.kt | sed -n '1,230p'
cat -n core/datastore/src/main/java/kr/co/call/datastore/AccountStateDataStore.kt | sed -n '1,80p'
echo
echo "===== Onboarding and MyPage consumers ====="
cat -n core/domain/src/main/java/kr/co/call/domain/repository/OnboardingRepository.kt
cat -n feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingViewModel.kt | sed -n '1,230p'
cat -n feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingViewModel.kt | sed -n '230,360p'
cat -n feature/mypage/impl/src/main/java/kr/co/call/impl/viewmodel/MyPageViewModel.kt | sed -n '1,140p'Repository: call-from-ai/CallFromAi_Android
Length of output: 27825
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "===== Login auth/initialization consumers for AccountStateDataStore ====="
rg -n -C 10 'AccountStateDataStore|Account.*State|shouldForceOnboarding|force_onboarding|forceOnboarding|tokenDataStore|getAccessToken|login|LoginRepositoryImpl|KakaoLogoutManager|onboardingEntry' app core feature -g '*.kt'
echo
echo "===== LoginRepositoryImpl.kt if present ====="
if [ -f core/data/src/main/java/kr/co/call/data/repositoryImpl/LoginRepositoryImpl.kt ]; then
cat -n core/data/src/main/java/kr/co/call/data/repositoryImpl/LoginRepositoryImpl.kt | sed -n '1,220p'
else
fd -t f -a 'LoginRepository.*\.kt' . | while read -r f; do echo "===== $f ====="; cat -n "$f" | sed -n '1,220p'; done
fiRepository: call-from-ai/CallFromAi_Android
Length of output: 50390
탈퇴의 부분 실패 처리를 분리하세요.
myPageApi.deleteAccount() 성공 후 tokenDataStore.clearTokens()가 실패하면 markWithdrawn()이 실행되지 않습니다. MyPageViewModel도 이후 카카오 로그아웃 실패를 기록만 하는 것과 달리 전체 탈퇴 실패로 반환합니다. 토큰 삭제 실패는 3rd-party 로그인 상태를 유지하는 비동기 처리로 줄이고, 탈퇴 완료 상태 저장은 토큰 정리와 분리하세요. 카카오토큰/출석 카드 실패도 실패로 반환하면 탈퇴가 취소 가능합니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@core/data/src/main/java/kr/co/call/data/repositoryImpl/MyPageRepositoryImpl.kt`
around lines 43 - 51, In the deleteAccount function, separate the withdrawal
completion state management from token cleanup to handle partial failures. After
myPageApi.deleteAccount() succeeds, call accountStateDataStore.markWithdrawn()
to record the withdrawal state regardless of subsequent token deletion. Move
tokenDataStore.clearTokens() to execute as an independent non-blocking operation
that catches and logs its own failures without propagating them to the overall
result, since token deletion should not prevent the withdrawal from being marked
complete or cancel the deletion operation.
| runRepositoryCatching { | ||
| // 1. 사용자 정보 저장 | ||
| safeApiCallUnit(errorResponseParser) { | ||
| myPageApi.updateMember( | ||
| request = UpdateMemberRequestDto( | ||
| lastName = submission.member.lastName, | ||
| firstName = submission.member.firstName, | ||
| imageUrl = submission.member.imageUrl, | ||
| gender = submission.member.gender, | ||
| birth = submission.member.birth, | ||
| mbti = submission.member.mbti, | ||
| job = submission.member.job, | ||
| ), | ||
| ) | ||
| } | ||
|
|
||
| // 2. 캐릭터 생성 | ||
| safeApiCallUnit(errorResponseParser) { | ||
| aiCharacterApi.createCharacter( | ||
| request = CreateCharacterRequestDto( | ||
| lastName = submission.character.lastName, | ||
| firstName = submission.character.firstName, | ||
| gender = submission.character.gender, | ||
| age = submission.character.age, | ||
| job = submission.character.job, | ||
| imageUrl = submission.character.imageUrl, | ||
| spiceLevel = submission.character.spiceLevel, | ||
| preferTime = submission.character.preferTime, | ||
| mbti = submission.character.mbti, | ||
| speechStyle = submission.character.speechStyle, | ||
| relationshipStage = | ||
| submission.character.relationshipStage, | ||
| traits = submission.character.traits.map { trait -> | ||
| CharacterTraitRequestDto( | ||
| trait = trait.trait, | ||
| priority = trait.priority, | ||
| ) | ||
| }, | ||
| ), | ||
| ) | ||
| } | ||
| accountStateDataStore.clearForceOnboarding() |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
# 온보딩 제출과 캐릭터 생성의 멱등성 또는 트랜잭션 계약을 확인합니다.
rg -n -C 5 \
-g '*.kt' -g '*.yaml' -g '*.yml' -g '*.json' -g '*.md' \
'submitOnboarding|createCharacter|POST\("characters"\)|idempoten|onboarding' . || trueRepository: call-from-ai/CallFromAi_Android
Length of output: 48904
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
echo "== OnboardingRepositoryImpl =="
cat -n core/data/src/main/java/kr/co/call/data/repositoryImpl/OnboardingRepositoryImpl.kt
echo
echo "== OnboardingViewModel submitOnboarding branch =="
sed -n '118,195p' feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingViewModel.kt | cat -n
echo
echo "== Force onboarding access =="
rg -n -C 4 'setForceOnboarding|clearForceOnboarding|FORCE_ONBOARDING|forceOnboarding' core data feature app | sed -n '1,220p' || true
echo
echo "== API interfaces around character/member requests =="
cat -n core/network/src/main/java/kr/co/call/network/api/AICharacterApi.kt
echo "---"
cat -n core/network/src/main/java/kr/co/call/network/api/MyPageApi.kt
echo "---"
cat -n core/network/src/main/java/kr/co/call/network/dto/onboarding/CreateCharacterRequestDto.kt
echo "---"
cat -n core/network/src/main/java/kr/co/call/network/dto/onboarding/UpdateMemberRequestDto.ktRepository: call-from-ai/CallFromAi_Android
Length of output: 13884
온보딩 제출을 원자적으로 처리하거나 캐릭터 생성을 멱등하게 만드세요.
submitOnboarding()은 updateMember() 이후 createCharacter()가 실패하면 clearForceOnboarding()을 호출하지 않아 온보딩 입력값이 저장되고 재시도에서도 POST("characters")를 다시 보내는 경로가 열립니다. 서버에서 온보딩 제출을 하나로 처리하거나, 캐릭터 생성 요청에 idempotency key 또는 upsert 계약을 추가해 이미 완료된 단계가 안전하게 재개되도록 해야 합니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@core/data/src/main/java/kr/co/call/data/repositoryImpl/OnboardingRepositoryImpl.kt`
around lines 26 - 67, Update submitOnboarding so the member update and character
creation are atomic, or make createCharacter idempotent by adding an idempotency
key or upsert contract. Ensure retries after updateMember succeeds do not create
duplicate characters and can safely resume the onboarding flow before
clearForceOnboarding() is called.
| ?: runBlocking{ | ||
| tokenDataStore.getAccessToken() | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate relevant files =="
fd -a 'AuthInterceptor\.kt|TokenDataStore' . | sed 's#^\./##' || true
echo "== inspect AuthInterceptor =="
for f in $(fd 'AuthInterceptor\.kt' .); do
echo "--- $f ($(wc -l < "$f") lines) ---"
cat -n "$f"
done
echo "== inspect TokenDataStore candidates =="
for f in $(fd 'TokenDataStore' .); do
echo "--- $f ($(wc -l < "$f") lines) ---"
cat -n "$f"
done
echo "== search for interceptor/cache usages =="
rg -n "AuthInterceptor|runBlocking|DataStore|getAccessToken|tokenCache|cache" -S core/network || trueRepository: call-from-ai/CallFromAi_Android
Length of output: 11391
interceptor에서 DataStore I/O를 동기 대기하지 마세요.
AuthInterceptor가 getCachedAccessToken() 실패 시 runBlocking { getAccessToken() }을 호출합니다. getAccessToken()은 safeData.first()까지 동기적으로 대기하므로, DataStore 파일 읽기가 지연되면 OkHttp 요청 스레드를 블로킹합니다. interceptor는 메모리 캐시만 읽도록 만들고, 캐시 미스 처리를 토큰 재부팅 경로에서 실행하거나 명시적 타임아웃과 실패 정책을 적용하세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@core/network/src/main/java/kr/co/call/network/interceptor/AuthInterceptor.kt`
around lines 26 - 28, Remove the runBlocking { tokenDataStore.getAccessToken() }
call from the AuthInterceptor fallback logic. The interceptor should only read
from memory cache using getCachedAccessToken() and avoid any synchronous
DataStore I/O operations that would block the OkHttp request thread. Instead,
move DataStore reads and token refresh operations to a dedicated token refresh
path that executes before requests are made, or implement explicit timeout and
failure policies at a higher level outside the interceptor itself.
Source: Path instructions
| const val DEFAULT_PROFILE_IMAGE_URL = | ||
| "https://callfromai-images.s3.ap-northeast-2.amazonaws.com/male_1.png" | ||
| val maleImages = List(5) { index -> | ||
| ProfileImageOption( | ||
| id = "temporary_male_${index + 1}", | ||
| imageUrl = "", | ||
| imageUrl = DEFAULT_PROFILE_IMAGE_URL, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
여성 프로필 이미지에도 제출 가능한 URL을 제공하세요.
Line 13은 남성 옵션에만 URL을 설정합니다. femaleImages는 빈 imageUrl을 유지합니다. 여성 이미지를 선택하면 Onboarding1Screen 또는 Onboarding2Screen이 빈 문자열을 전달하고, OnboardingViewModel이 이를 API 요청에 포함합니다. 모든 선택 가능한 여성 옵션에 유효한 URL을 지정하세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TemporaryProfileImageData.kt`
around lines 8 - 13, Update the femaleImages definitions alongside the
maleImages list in the temporary profile image data so every selectable female
ProfileImageOption receives a valid imageUrl instead of an empty string. Reuse
the appropriate existing profile image URL constant or provide the corresponding
female image URL while preserving the existing option IDs and list structure.
| selectedOption = selectedJob?.label.orEmpty(), | ||
| placeholder = "직업을 선택해주세요", | ||
| options = listOf("대학생", "직장인", "기타",), | ||
| onOptionSelected = { job = it }, | ||
| onOptionSelected = {selectedLabel -> | ||
| selectedJob=MemberJob.entries.firstOrNull(){job-> | ||
| job.label == selectedLabel | ||
| } | ||
| }, |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
화면 변경 증빙을 PR 설명에 추가하세요.
제공된 PR 설명에는 스크린샷 또는 화면 녹화가 없습니다.
feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt#L183-L190: 회원 직업과 프로필 선택 동작을 보여주세요.feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.kt#L184-L191: AI 직업 선택과 다음 단계 활성화 동작을 보여주세요.feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding6Screen.kt#L98-L100: 제출 중 클릭 차단 동작을 보여주세요.
As per coding guidelines, “Jetpack Compose 화면 파일이 변경된 경우, PR 설명에 스크린샷 또는 화면 녹화가 첨부되었는지 확인하고 없으면 경고한다.”
📍 Affects 3 files
feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt#L183-L190(this comment)feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.kt#L184-L191feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding6Screen.kt#L98-L100
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt`
around lines 183 - 190, PR 설명에 화면 변경 증빙을 추가하세요. Onboarding1Screen.kt 183-190에서는
회원 직업 및 프로필 선택 동작을, Onboarding2Screen.kt 184-191에서는 AI 직업 선택과 다음 단계 활성화를,
Onboarding6Screen.kt 98-100에서는 제출 중 클릭 차단 동작을 보여주는 스크린샷 또는 화면 녹화를 각각 첨부하세요.
Source: Coding guidelines
| fun submitOnboarding( | ||
| callNow: Boolean, | ||
| ) = intent { | ||
| if (state.submitStatus == LoadStatus.Loading) return@intent | ||
|
|
||
| val age = state.aiAge.toIntOrNull() | ||
| val speechStyle = state.speechStyle | ||
| val relationship = state.relationship | ||
| val preferTime = state.preferTime | ||
|
|
||
| if ( | ||
| age == null || | ||
| speechStyle == null || | ||
| relationship == null || | ||
| preferTime == null || | ||
| state.traits.isEmpty() | ||
| ) { | ||
| postSideEffect( | ||
| OnboardingSideEffect.ShowMessage( | ||
| message = "온보딩 정보를 확인해주세요.", | ||
| ), | ||
| ) | ||
| return@intent | ||
| } | ||
|
|
||
| reduce { | ||
| state.copy(submitStatus = LoadStatus.Loading) | ||
| } | ||
|
|
||
| val submission = OnboardingSubmission( | ||
| member = MemberOnboardingInput( | ||
| lastName = state.userLastName, | ||
| firstName = state.userFirstName, | ||
| imageUrl = state.userImageUrl, | ||
| gender = state.userGender, | ||
| birth = state.userBirthday.toString(), | ||
| mbti = state.userMbti, | ||
| job = state.userJob, | ||
| ), | ||
| character = CharacterOnboardingInput( | ||
| lastName = state.aiLastName, | ||
| firstName = state.aiFirstName, | ||
| gender = state.aiGender, | ||
| age = age, | ||
| job = state.aiJob, | ||
| imageUrl = state.aiImageUrl, | ||
| spiceLevel = state.temperature, | ||
| preferTime = preferTime.name, | ||
| mbti = state.aiMbti, | ||
| speechStyle = speechStyle.name, | ||
| relationshipStage = relationship.name, | ||
| traits = state.traits.mapIndexed { index, trait -> | ||
| CharacterTraitInput( | ||
| trait = trait.keyword, | ||
| priority = index + 1, | ||
| ) | ||
| }, | ||
| ), | ||
| ) | ||
|
|
||
| onboardingRepository.submitOnboarding(submission) | ||
| .onSuccess { | ||
| reduce { | ||
| state.copy(submitStatus = LoadStatus.Idle) | ||
| } | ||
|
|
||
| postSideEffect( | ||
| OnboardingSideEffect.OnboardingCompleted( | ||
| callNow = callNow, | ||
| ), | ||
| ) | ||
| } | ||
| .onFailure { error -> | ||
| if (error is CancellationException) throw error | ||
|
|
||
| reduce { | ||
| state.copy(submitStatus = LoadStatus.Idle) | ||
| } | ||
|
|
||
| postSideEffect( | ||
| OnboardingSideEffect.ShowMessage( | ||
| message = error.toUserMessage( | ||
| default = "온보딩 저장에 실패했습니다.", | ||
| ), | ||
| ), | ||
| ) | ||
| } | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
온보딩 제출 로직을 UseCase로 위임하세요.
Line 118부터 Line 205까지의 ViewModel이 입력 검증, 제출 모델 조립, 우선순위 생성, Repository 호출을 모두 수행합니다. SubmitOnboardingUseCase로 도메인 제출 규칙과 저장을 이동하세요. ViewModel은 UI 상태 변환, reduce, postSideEffect만 처리하세요.
As per path instructions, “비즈니스 로직이 UseCase(core:domain) 호출로 위임되고 ViewModel에 과도하게 몰려있지 않은지 확인.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingViewModel.kt`
around lines 118 - 205, Extract the validation, OnboardingSubmission
construction, trait priority assignment, and
onboardingRepository.submitOnboarding call from submitOnboarding into a
core-domain SubmitOnboardingUseCase. Update the ViewModel’s submitOnboarding
method to pass the required state inputs and call the use case, retaining only
UI state handling through reduce and postSideEffect, including loading, success,
cancellation, and error presentation.
Source: Path instructions
There was a problem hiding this comment.
@dada4679, 알겠습니다. 이 리팩터링 제안은 무시하겠습니다.
ʕ •́ᴥ•̀ʔ
You are interacting with an AI system.
codebidoof
left a comment
There was a problem hiding this comment.
디벨롭 풀 받구 아원이가 개선해놓은 safeApiCall 쓰면 될 것 같아!
| override suspend fun logout():Result<Unit> = | ||
| runRepositoryCatching { | ||
| safeEmptyApiCall(errorResponseParser){ | ||
| myPageApi.logout() | ||
| } | ||
| //서버 로그아웃 성공 후 기기 토큰 삭제 | ||
| tokenDataStore.clearTokens() | ||
| } | ||
| override suspend fun deleteAccount(): Result<Unit> = | ||
| runRepositoryCatching { | ||
| //토큰이 남아 있을 때 탈퇴 api를 먼저 호출 | ||
| safeApiCallUnit(errorResponseParser){ | ||
| myPageApi.deleteAccount() | ||
| } | ||
| //서버 탈퇴 성공 후 기기의 토큰 삭제 | ||
| tokenDataStore.clearTokens() | ||
| accountStateDataStore.markWithdrawn() | ||
| } | ||
|
|
There was a problem hiding this comment.
이거 이제 아원이가 수정을 해서 runRepositoryCatching 없이 safeApiCall류의 함수만 호출해도 될 거야!!
| @Singleton | ||
| class AccountStateDataStore @Inject constructor( | ||
| private val dataStore: DataStore<Preferences>, | ||
| ){ | ||
| suspend fun markWithdrawn(){ | ||
| dataStore.edit {preferences-> | ||
| preferences[FORCE_ONBOARDING]=true | ||
| } | ||
| } | ||
| suspend fun shouldForceOnboarding(): Boolean{ | ||
| return dataStore.data.first()[FORCE_ONBOARDING] ?:false | ||
| } | ||
| suspend fun clearForceOnboarding() { | ||
| dataStore.edit { preferences -> | ||
| preferences.remove(FORCE_ONBOARDING) | ||
| } | ||
| } | ||
|
|
||
| private companion object { | ||
| val FORCE_ONBOARDING = | ||
| booleanPreferencesKey("force_onboarding_after_withdrawal") | ||
| } | ||
| } No newline at end of file |
There was a problem hiding this comment.
아마 이거 정확한 맥락은 모르겠는데 회원탈퇴 or 로그아웃 후 다시 로그인할 시에 온보딩을 보여지게 하는 것을 강제하기 위함 같은데, needsOnboarding 필드를 로그인시에 백엔드에서 내려주기 때문에 이 로직은 필요 없어 보입니다!
| data class OnboardingSubmission( | ||
| val member: MemberOnboardingInput, | ||
| val character: CharacterOnboardingInput, | ||
| ) | ||
|
|
||
| data class MemberOnboardingInput( | ||
| val lastName: String, | ||
| val firstName: String, | ||
| val imageUrl: String, | ||
| val gender: String, | ||
| val birth: String, | ||
| val mbti: String, | ||
| val job: String, | ||
| ) | ||
|
|
||
| data class CharacterOnboardingInput( | ||
| val lastName: String, | ||
| val firstName: String, | ||
| val gender: String, | ||
| val age: Int, | ||
| val job: String, | ||
| val imageUrl: String, | ||
| val spiceLevel: Int, | ||
| val preferTime: String, | ||
| val mbti: String, | ||
| val speechStyle: String, | ||
| val relationshipStage: String, | ||
| val traits: List<CharacterTraitInput>, | ||
| ) | ||
|
|
||
| data class CharacterTraitInput( | ||
| val trait: String, | ||
| val priority: Int, | ||
| ) No newline at end of file |
|
|
||
| // retrofit, okhttp | ||
| implementation(libs.retrofit) | ||
| api(libs.retrofit) |
| data class CreateCharacterRequestDto( | ||
| val lastName: String, | ||
| val firstName: String, | ||
| val gender: String, | ||
| val age: Int, | ||
| val job: String, | ||
| val imageUrl: String, | ||
| val spiceLevel: Int, | ||
| val preferTime: String, | ||
| val mbti: String, | ||
| val speechStyle: String, | ||
| val relationshipStage: String, | ||
| val traits: List<CharacterTraitRequestDto> | ||
| ) | ||
|
|
||
| data class CharacterTraitRequestDto( | ||
| val trait: String, | ||
| val priority: Int, | ||
| ) No newline at end of file |
Gemini AI 코드리뷰안녕하세요! PR 잘 올려주셨습니다. 전체적으로 MVI 패턴과 Clean Architecture 원칙을 잘 적용하고 계셔서 인상 깊습니다. 특히 전역 세션 만료 처리, DataStore 백업 제외, Repository 레이어의 1. Kotlin 코드 (일반)
2. Jetpack Compose 코드
3. Repository/DataSource 레이어
4. ViewModel
5. FCM/SSE/실시간 통신 관련 코드
그 외 잡다한 코멘트
전반적으로 품질 높은 PR입니다. 위에서 언급한 몇 가지 개선사항들(특히 수고하셨습니다! |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
core/network/src/main/java/kr/co/call/network/interceptor/TokenAuthenticator.kt (1)
172-184: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
clearTokens()실패 시 세션 만료 알림이 전달되지 않습니다.Refresh Token까지 만료되어 401을 받은 경우,
authSessionManager.notifySessionExpired()가tokenDataStore.clearTokens()와 같은try블록 안에 있습니다.clearTokens()가 예외를 던지면catch로 이동해 알림 호출이 실행되지 않습니다. 이 경우 세션은 실질적으로 만료되었는데도 로그인 화면으로 이동하지 않아 사용자가 인증 오류 상태에 머무를 수 있습니다.🛡️ 제안하는 수정
if (reissueResponse.code() == 401) { try { tokenDataStore.clearTokens() - authSessionManager.notifySessionExpired() } catch (error: CancellationException) { throw error } catch (error: Throwable) { Timber.e( error, "만료된 토큰 삭제 실패", ) + } finally { + authSessionManager.notifySessionExpired() } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@core/network/src/main/java/kr/co/call/network/interceptor/TokenAuthenticator.kt` around lines 172 - 184, Separate token cleanup from session-expiration notification in the 401 handling within TokenAuthenticator: ensure authSessionManager.notifySessionExpired() is invoked even when tokenDataStore.clearTokens() throws. Preserve CancellationException propagation and continue logging other cleanup failures without preventing the expiration notification.feature/mypage/impl/src/main/java/kr/co/call/impl/viewmodel/MyPageViewModel.kt (1)
105-130: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
deleteAccount()에서 카카오 로그아웃 실패가 무시된 채 로그인 화면으로 이동합니다.
logout()은 이미 카카오 로그아웃 실패 시NavigateToLogin을 막고 오류 메시지를 표시하도록 수정되었습니다.deleteAccount()는 동일한 패턴을 적용하지 않았습니다.kakaoLogoutManager.logout()실패는 로그로만 남기고 항상postSideEffect(MyPageSideEffect.NavigateToLogin)을 호출합니다.계정은 이미 삭제되어 API 재시도 대상은 아니지만, 카카오 SDK 세션이 로컬에 남으면 다음 로그인 시도에서 자동 로그인이 발생해 삭제된 계정 흐름과 불일치할 수 있습니다.
KakaoLogoutManager에unlink()와 같은 강제 무효화 수단이 있는지 확인하고, 실패 시 처리를logout()과 일관되게 맞추세요.#!/bin/bash set -euo pipefail echo "== KakaoLogoutManager 구현 확인 ==" fd -a 'KakaoLogoutManager.kt' --exec cat -n {}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@feature/mypage/impl/src/main/java/kr/co/call/impl/viewmodel/MyPageViewModel.kt` around lines 105 - 130, Update deleteAccount() to handle kakaoLogoutManager.logout() failures consistently with the existing logout flow: do not navigate to login when logout fails, show the appropriate error message, and use KakaoLogoutManager.unlink() or another available forced invalidation method if needed. Preserve the successful logout path’s NavigateToLogin side effect and existing account-deletion failure handling.
🧹 Nitpick comments (1)
core/datastore/src/main/java/kr/co/call/datastore/TokenDataStore.kt (1)
30-46: 🔒 Security & Privacy | 🔵 Trivial참고: Access/Refresh Token이 평문 Preferences에 저장됩니다.
이번 diff는 함수명만
saveTokens에서setTokens로 바꾼 것이라 새로운 결함은 아닙니다. 다만core/datastore경로 지침은 민감 데이터의 암호화 여부 확인을 요구합니다. 토큰이 평문stringPreferencesKey로 저장되므로, 백업 제외 설정과 별개로 기기 루팅이나 파일 시스템 접근 시 노출될 수 있습니다. 장기적으로androidx.security.crypto등을 이용한 암호화 저장을 검토하세요.As per path instructions, "토큰/인증 정보 등 민감 데이터를 평문 DataStore에 저장하지 않는지(암호화 여부) 확인."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@core/datastore/src/main/java/kr/co/call/datastore/TokenDataStore.kt` around lines 30 - 46, Update setTokens and the corresponding token retrieval flow to store ACCESS_TOKEN and REFRESH_TOKEN through encrypted storage rather than plain string Preferences, using the project’s supported security mechanism such as androidx.security.crypto. Preserve the existing validation and token access behavior while ensuring both sensitive values are encrypted at rest.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/src/main/res/xml/backup_rules.xml`:
- Around line 13-15: Update the exclusion path in
app/src/main/res/xml/backup_rules.xml lines 13-15 and
app/src/main/res/xml/data_extraction_rules.xml lines 12-13 to
datastore/call_from_ai_token_prefs.preferences_pb, matching the file name
created by TokenDataStore; both sites require the same direct change.
In `@core/datastore/src/main/java/kr/co/call/datastore/TokenDataStore.kt`:
- Around line 25-27: Update getTokens() to catch DataStore read/I/O exceptions
from dataStore.data.first() and return an empty-token result equivalent to
emptyPreferences() when reading fails. Keep the normal toStoredTokens()
conversion unchanged for successful reads, so AuthInterceptor.intercept(),
TokenAuthenticator.authenticate(), and
LandingViewModel.checkAutoLoginAfterSplash() receive a safe fallback instead of
an uncaught exception.
---
Outside diff comments:
In
`@core/network/src/main/java/kr/co/call/network/interceptor/TokenAuthenticator.kt`:
- Around line 172-184: Separate token cleanup from session-expiration
notification in the 401 handling within TokenAuthenticator: ensure
authSessionManager.notifySessionExpired() is invoked even when
tokenDataStore.clearTokens() throws. Preserve CancellationException propagation
and continue logging other cleanup failures without preventing the expiration
notification.
In
`@feature/mypage/impl/src/main/java/kr/co/call/impl/viewmodel/MyPageViewModel.kt`:
- Around line 105-130: Update deleteAccount() to handle
kakaoLogoutManager.logout() failures consistently with the existing logout flow:
do not navigate to login when logout fails, show the appropriate error message,
and use KakaoLogoutManager.unlink() or another available forced invalidation
method if needed. Preserve the successful logout path’s NavigateToLogin side
effect and existing account-deletion failure handling.
---
Nitpick comments:
In `@core/datastore/src/main/java/kr/co/call/datastore/TokenDataStore.kt`:
- Around line 30-46: Update setTokens and the corresponding token retrieval flow
to store ACCESS_TOKEN and REFRESH_TOKEN through encrypted storage rather than
plain string Preferences, using the project’s supported security mechanism such
as androidx.security.crypto. Preserve the existing validation and token access
behavior while ensuring both sensitive values are encrypted at rest.
🪄 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: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 14ea7d68-b7a2-4a65-9980-da1da44bec7b
📒 Files selected for processing (24)
app/src/main/java/kr/co/call/callfromai/AppScreen.ktapp/src/main/java/kr/co/call/callfromai/AppViewModel.ktapp/src/main/java/kr/co/call/callfromai/sideeffect/AppSideEffect.ktapp/src/main/res/xml/backup_rules.xmlapp/src/main/res/xml/data_extraction_rules.xmlcore/data/src/main/java/kr/co/call/data/di/RepositoryModule.ktcore/data/src/main/java/kr/co/call/data/repositoryImpl/LoginRepositoryImpl.ktcore/data/src/main/java/kr/co/call/data/repositoryImpl/MyPageRepositoryImpl.ktcore/data/src/main/java/kr/co/call/data/repositoryImpl/OnboardingRepositoryImpl.ktcore/data/src/main/java/kr/co/call/data/util/SafeApiResult.ktcore/datastore/src/main/java/kr/co/call/datastore/TokenDataStore.ktcore/domain/src/main/java/kr/co/call/domain/model/onboarding/OnboardingInput.ktcore/domain/src/main/java/kr/co/call/domain/repository/OnboardingRepository.ktcore/network/src/main/java/kr/co/call/network/api/MyPageApi.ktcore/network/src/main/java/kr/co/call/network/interceptor/AuthInterceptor.ktcore/network/src/main/java/kr/co/call/network/interceptor/TokenAuthenticator.ktfeature/login/impl/src/main/java/kr/co/call/impl/viewmodel/LandingViewModel.ktfeature/mypage/impl/src/main/java/kr/co/call/impl/viewmodel/MyPageViewModel.ktfeature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.ktfeature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.ktfeature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding5Screen.ktfeature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingSideEffect.ktfeature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingViewModel.ktfeature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/model/Job.kt
💤 Files with no reviewable changes (1)
- core/data/src/main/java/kr/co/call/data/di/RepositoryModule.kt
🚧 Files skipped from review as they are similar to previous changes (4)
- feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/model/Job.kt
- core/network/src/main/java/kr/co/call/network/api/MyPageApi.kt
- core/network/src/main/java/kr/co/call/network/interceptor/AuthInterceptor.kt
- feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.kt
| suspend fun getTokens(): StoredTokens { | ||
| return dataStore.data.first().toStoredTokens() | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== DataStore 생성/corruptionHandler 설정 확인 =="
rg -n -C5 'preferencesDataStore|DataStoreFactory|PreferenceDataStoreFactory|corruptionHandler' --type=kt
echo "== getTokens 호출처 확인 =="
rg -n -C3 '\.getTokens\(' --type=ktRepository: call-from-ai/CallFromAi_Android
Length of output: 243
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Locate TokenDataStore.kt =="
fd -a 'TokenDataStore.kt' . || true
echo "== DataStore creation/settings in Kotlin files =="
rg -n -C5 'preferencesDataStore|DataStoreFactory|PreferenceDataStoreFactory|corruptionHandler|DataStore<|DataStorePreferences' -g '*.kt' . || true
echo "== getTokens callers in Kotlin files =="
rg -n -C3 'getTokens\(' -g '*.kt' . || true
echo "== token-related DataStore files =="
rg -n -C3 'TokenDataStore|StoredTokens|PreferenceDataStoreFactory|corruptionHandler|dataStore\.data' -g '*.kt' . || trueRepository: call-from-ai/CallFromAi_Android
Length of output: 20653
🌐 Web query:
AndroidX DataStore Preferences ReplaceFileCorruptionHandler IOException catch behavior documentation
💡 Result:
In AndroidX DataStore, the ReplaceFileCorruptionHandler is designed to manage scenarios where data corruption (indicated by a CorruptionException) prevents the reading of a file [1][2]. Its behavior regarding exceptions encountered during the recovery process is as follows: If the ReplaceFileCorruptionHandler succeeds in replacing the corrupted data with new data (via its produceNewData logic), the original CorruptionException is considered handled and is swallowed [2][3]. If the ReplaceFileCorruptionHandler encounters an exception while attempting to replace the data, that new exception is added as a suppressed exception to the original CorruptionException, and the original exception is then thrown [2][3]. Additionally, if the handleCorruption function itself throws an exception during its execution, that exception will be propagated to the original call to the DataStore that triggered the corruption handling [1][2]. It is important to note that the handleCorruption function should not interact with any DataStore API, as doing so can result in a deadlock [1][3]. The CorruptionException, which triggers this process, is a subclass of IOException and is specifically thrown by serializers when data cannot be deserialized, rather than for transient IO or permission issues [4].
Citations:
- 1: https://developer.android.com/reference/androidx/datastore/core/CorruptionHandler
- 2: https://developer.android.com/reference/androidx/datastore/core/handlers/ReplaceFileCorruptionHandler
- 3: https://developer.android.com/reference/kotlin/androidx/datastore/core/handlers/ReplaceFileCorruptionHandler
- 4: https://developer.android.com/reference/androidx/datastore/core/CorruptionException
🌐 Web query:
AndroidX DataStore data Flow catches IOException catch returns from flow documentation ReadPreferences
💡 Result:
When reading data from AndroidX DataStore using the data property, the returned Flow can throw an IOException if an error occurs while reading from disk [1]. To handle these errors effectively, official documentation and standard practices recommend using the catch Flow operator before applying map [2]. Within the catch block, you should inspect the exception: if it is an IOException, you can log the error and emit a default value (such as emptyPreferences) to allow the flow to continue [3][2]. If the exception is of any other type, it should typically be re-thrown to avoid masking unexpected issues [2][4]. Recommended implementation pattern: val userPreferencesFlow: Flow = dataStore.data.catch { exception -> if (exception is IOException) { // Log the error and emit a default/empty value emit(emptyPreferences) } else { // Re-throw unexpected exceptions throw exception } }.map { preferences -> // Proceed with mapping to your data model val exampleValue = preferences[EXAMPLE_KEY]?: defaultValue UserPreferences(exampleValue) } This approach ensures that your application remains resilient to transient I/O errors while still exposing critical failures for proper debugging [2]. For cases involving file corruption, you may also consider configuring a CorruptionHandler when initializing your DataStore instance to automatically replace corrupted files [5].
Citations:
- 1: https://developer.android.com/reference/kotlin/androidx/datastore/core/DataStore
- 2: https://developer.android.google.cn/codelabs/android-preferences-datastore
- 3: https://gist.github.com/simona-anomis/a38b35a3bccd91c44204d181401bd65e
- 4: https://android.googlesource.com/platform/frameworks/support/+/690c6ab7ecd7cd8566398e183f06a1ffccff5bdc/datastore/datastore-sampleapp/src/main/java/com/example/datastoresampleapp/KotlinSerializationActivity.kt
- 5: https://developer.android.com/topic/libraries/architecture/datastore
🌐 Web query:
DataStore Core ReadPreferences catch IOException and rethrow corruption exception source
💡 Result:
In Android DataStore, a CorruptionException is a specific subclass of IOException used to signal that the data on disk cannot be deserialized [1]. When DataStore encounters this error during a read, it invokes a configured CorruptionHandler [2][3]. If no custom handler is provided, DataStore effectively uses the default ReThrowCorruptionHandler, which does nothing but rethrow the CorruptionException, causing it to propagate to the original caller [4][5]. Developers can customize this behavior by providing a CorruptionHandler during DataStore creation [3]. For example, the ReplaceFileCorruptionHandler is a common implementation that attempts to recover by replacing the corrupted file with new, default data [6][7]. Key points regarding this mechanism include: 1. CorruptionException vs. Generic IOException: CorruptionException should only be thrown by a serializer when data is unparseable or corrupted [8][1]. It should not be thrown for transient IO issues, such as file permissions or disk availability [1]. 2. Propagation: If the handleCorruption function itself throws an exception, that exception is propagated to the original DataStore caller [2][9]. 3. Deadlocks: CorruptionHandlers must not interact with any DataStore API, as doing so can result in a deadlock [4][2][9]. 4. Implementation Details: When using ReplaceFileCorruptionHandler, if the handler successfully replaces the data, the original exception is swallowed; if the handler fails, the new exception is added as a suppressed exception to the original and the original exception is thrown [6][7].
Citations:
- 1: https://developer.android.com/reference/androidx/datastore/core/CorruptionException
- 2: https://developer.android.com/reference/kotlin/androidx/datastore/core/CorruptionHandler
- 3: https://developer.android.com/topic/libraries/architecture/datastore
- 4: https://developer.android.com/reference/kotlin/androidx/datastore/core/handlers/ReThrowCorruptionHandler
- 5: https://developer.android.com/reference/androidx/datastore/core/handlers/ReThrowCorruptionHandler
- 6: https://android.googlesource.com/platform/frameworks/support/+/f2e05c341382db64d127118a13451dcaa554b702/datastore/datastore-core/src/main/java/androidx/datastore/core/handlers/ReplaceFileCorruptionHandler.kt
- 7: https://developer.android.com/reference/androidx/datastore/core/handlers/ReplaceFileCorruptionHandler
- 8: https://developer.android.com/reference/androidx/datastore/preferences/core/PreferencesSerializer
- 9: https://developer.android.com/reference/androidx/datastore/core/CorruptionHandler
🌐 Web query:
DataStore core ReadPreferences catch IOException read exceptions catchCorruption propagate catch IOException source
💡 Result:
In Jetpack DataStore, IOException and its subclass CorruptionException are handled through different mechanisms depending on the cause of the failure [1][2]. General IOException Handling DataStore operations, such as reading data via the flow property or writing via the updateData function, will throw an IOException if a disk-related error occurs [3][4]. Because the read API exposes a Flow, developers are expected to handle these exceptions using standard flow operators, such as the catch operator, directly in their implementation [5]. If a read fails, the exception is propagated to the collector of the flow, and subsequent collection attempts will retry the read [3][4]. Corruption Handling When data cannot be deserialized, the serializer throws a CorruptionException [6][7][2]. This specific type of IOException is managed by a CorruptionHandler defined at the time of DataStore creation [8][9]. 1. CorruptionHandler: This interface defines the handleCorruption(ex: CorruptionException) method, which DataStore calls when corruption is detected [9][10]. 2. Propagation: If the handleCorruption function itself throws an exception, that exception is propagated to the original caller of the DataStore operation [9][11][12]. 3. Recovery: If handleCorruption returns a value (e.g., a default or empty state), DataStore attempts to write that value to disk, potentially recovering from the corruption [9][11][10]. 4. Built-in Handlers: - ReThrowCorruptionHandler: The default handler that simply rethrows the CorruptionException, effectively propagating it to the caller [9][12]. - ReplaceFileCorruptionHandler: Attempts to replace the corrupted file with a new, provided value [11][10]. If this replacement process itself fails, the new exception is added as a suppressed exception to the original CorruptionException, which is then thrown [11]. It is important to note that CorruptionException should only be thrown by serializers when data is unparseable or corrupted [2]. Transient I/O issues or file system permissions errors should not be wrapped in a CorruptionException, as these should remain as generic IOExceptions [7][2]. Additionally, corruption handlers should never interact with DataStore APIs directly to avoid deadlocks [9][11].
Citations:
- 1: https://developer.android.com/reference/androidx/datastore/core/IOException
- 2: https://developer.android.com/reference/kotlin/androidx/datastore/core/CorruptionException
- 3: https://developer.android.com/reference/kotlin/androidx/datastore/core/DataStore
- 4: https://developer.android.com/reference/androidx/datastore/core/DataStore
- 5: https://medium.com/androiddevelopers/all-about-preferences-datastore-cc7995679334
- 6: https://developer.android.com/reference/androidx/datastore/preferences/core/PreferencesFileSerializer
- 7: https://developer.android.com/reference/kotlin/androidx/datastore/core/Serializer
- 8: https://developer.android.com/reference/androidx/datastore/preferences/core/PreferenceDataStoreFactory
- 9: https://developer.android.com/reference/kotlin/androidx/datastore/core/CorruptionHandler
- 10: https://developer.android.com/reference/androidx/datastore/core/CorruptionHandler
- 11: https://developer.android.com/reference/kotlin/androidx/datastore/core/handlers/ReplaceFileCorruptionHandler
- 12: https://developer.android.com/reference/androidx/datastore/core/handlers/ReThrowCorruptionHandler
getTokens()에서 DataStore 예외를 호출 경로에서 처리해 주세요.
corruptionHandler는 파일 취소 값 복구를 위한 별도 경로를 담당하지만, 토큰 저장 파일의 기본 I/O 읽기 실패는 corruptionHandler만으로 잡히지 않습니다. dataStore.data는 Disk 관련 오류 시 호출자로 전파될 수 있습니다.
수신 경로인 AuthInterceptor.intercept(), TokenAuthenticator.authenticate(), LandingViewModel.checkAutoLoginAfterSplash()에서는 실제 예외 처리 없이 호출하기 때문에, 이 경로에서 emptyPreferences() 반환과 같은 처리가 필요합니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@core/datastore/src/main/java/kr/co/call/datastore/TokenDataStore.kt` around
lines 25 - 27, Update getTokens() to catch DataStore read/I/O exceptions from
dataStore.data.first() and return an empty-token result equivalent to
emptyPreferences() when reading fails. Keep the normal toStoredTokens()
conversion unchanged for successful reads, so AuthInterceptor.intercept(),
TokenAuthenticator.authenticate(), and
LandingViewModel.checkAutoLoginAfterSplash() receive a safe fallback instead of
an uncaught exception.
Gemini AI 코드리뷰Gemini API 응답을 읽지 못했습니다. {
"error": {
"code": 503,
"message": "This model is currently experiencing high demand. Spikes in demand are usually temporary. Please try again later.",
"status": "UNAVAILABLE"
}
} |
Gemini AI 코드리뷰안녕하세요. 시니어 Android 개발자로서 GitHub Pull Request를 리뷰해 드리겠습니다. 전반적으로 코드 품질 향상과 기능 추가를 위한 좋은 변경들이 많이 보입니다. 특히 DataStore 백업 규칙 적용, Orbit MVI 패턴을 사용한 SideEffect 처리, Repository 계층의 견고한 에러 핸들링, 그리고 Compose UI의 로딩 상태 관리 및 타입 안전성 개선 노력이 돋보입니다. 몇 가지 개선할 점과 질문이 있으니 아래 내용을 참고해 주세요. 전반적인 의견 (General Comments)
Kotlin & Android Core
|
Gemini AI 코드리뷰안녕하세요! 시니어 Android 개발자로서 GitHub PR을 리뷰해 드리겠습니다. 이번 PR은 온보딩 기능 추가, 마이페이지 로그아웃/회원 탈퇴 기능 구현, 그리고 인증 토큰 관리 로직 개선 등 여러 중요한 변경 사항을 포함하고 있네요. 전체적으로 비즈니스 로직과 UI 상태 관리에 몇 가지 개선점과 질문 사항들을 정리해 보았습니다. 긍정적인 변경 사항 (Good Points)
개선이 필요한 부분 및 질문 사항1. Kotlin 코드 리뷰
2. Jetpack Compose 코드 리뷰
3. Repository/DataSource 레이어
4. ViewModel
5. FCM/SSE/실시간 통신 관련 코드
결론 및 최종 권고전반적으로 이번 PR은 새로운 기능을 구현하면서 아키텍처 원칙 (클린 아키텍처, MVI with Orbit, Jetpack Compose)을 매우 잘 준수하고 있습니다. 특히 에러 핸들링, 로딩 상태 관리, UI와 비즈니스 로직 분리, Compose의 상태 호이스팅 등 핵심적인 부분에서 모범적인 코드를 보여주고 있습니다. 몇 가지 질문과 고려 사항 (특히 다음 사항에 대한 추가 검토 또는 논의를 권장합니다:
이 외의 변경 사항들은 매우 훌륭하며, 위 항목들에 대한 논의 및 개선이 이루어진다면 PR 승인에 문제가 없을 것 같습니다. 수고 많으셨습니다! |
codebidoof
left a comment
There was a problem hiding this comment.
아원이가 만든 메서드 사용하는 식으로 고쳐주면 될 것 같아!
| //예외처리파일 | ||
| /** | ||
| * Repository 작업에서 발생한 예외를 앱의 도메인 예외로 변환한다. | ||
| * 코루틴 취소와 치명적인 [Error]는 [toAppResult]에서 다시 전파된다. | ||
| */ | ||
| suspend fun <T> runRepositoryCatching( | ||
| block: suspend () -> T, | ||
| ): Result<T> = | ||
| runCatching { | ||
| block() | ||
| }.toAppResult() | ||
|
|
There was a problem hiding this comment.
이거 추가할 필요 없이 그냥 아원이가 만들어놓은거 쓰면 될 것 같아!
There was a problem hiding this comment.
아원이가 safeApiCall류 메서드들 반환타입 전부 Result로 맞춰 놨으니까 여기서 runCatching이랑 toAppResult() 쓰지 말고 safeApiCall만 호출해서 성공하면 dto에 오는 토큰 datastore에 캐싱하는 식으로 해 주면 될 것 같아
| suspend fun safeEmptyApiCall( | ||
| parser: ErrorResponseParser, | ||
| call: suspend()-> Response<Unit>, | ||
| ){ | ||
| try { | ||
| val response=call() | ||
| if (!response.isSuccessful){ | ||
| throw HttpException(response) | ||
| } | ||
| }catch (e: CancellationException){ | ||
| throw e | ||
| }catch (e: ApiException){ | ||
| throw e | ||
| }catch(e: HttpException){ | ||
| throw e.toApiException(parser) | ||
| } | ||
| } |
There was a problem hiding this comment.
이것도 추가할 필요 없이 그냥 아원이 꺼 쓰면 될 것 같아~!
Gemini AI 코드리뷰안녕하세요, PR 리뷰 요청해주셔서 감사합니다. PR 내용을 보니, 앱 전반에 걸쳐 인증 세션 만료 처리, 온보딩 기능 추가, 마이페이지 로그아웃/탈퇴 기능 개선 등 중요한 업데이트가 많네요. 전반적으로 코드 품질 향상을 위한 노력과 Jetpack Compose, Orbit MVI 패턴 적용이 잘 되어 있어 긍정적입니다. 몇 가지 개선 사항과 고려할 점들을 아래와 같이 정리했습니다. 1. 전반적인 피드백 (General Feedback)
2. 상세 코드 리뷰2.1 Kotlin 코드 리뷰
2.2 Jetpack Compose 코드 리뷰
2.3 Repository/DataSource Layer
2.4 ViewModel
2.5 FCM/SSE/실시간 통신 관련 코드
3. 총평이번 PR은 특히 다시 한번 수고하셨습니다! 👍 |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/src/main/res/xml/data_extraction_rules.xml`:
- Around line 12-13: Update the data extraction rules to enable the
device-transfer configuration and exclude
datastore/call_from_ai_token_prefs.preferences_pb there as well as in the
existing cloud-backup rules, preserving the token exclusion for both backup
paths.
🪄 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: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 88d3afa8-a853-4772-873e-3730ddd38700
📒 Files selected for processing (12)
app/src/main/res/xml/backup_rules.xmlapp/src/main/res/xml/data_extraction_rules.xmlcore/data/src/main/java/kr/co/call/data/repositoryImpl/LoginRepositoryImpl.ktcore/data/src/main/java/kr/co/call/data/repositoryImpl/MyPageRepositoryImpl.ktcore/data/src/main/java/kr/co/call/data/repositoryImpl/OnboardingRepositoryImpl.ktcore/domain/src/main/java/kr/co/call/domain/model/onboarding/OnboardingInput.ktcore/domain/src/main/java/kr/co/call/domain/repository/OnboardingRepository.ktcore/network/src/main/java/kr/co/call/network/api/MyPageApi.ktcore/network/src/main/java/kr/co/call/network/util/SafeApiCall.ktfeature/login/impl/src/main/java/kr/co/call/impl/viewmodel/LoginViewModel.ktfeature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingViewModel.ktfeature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/model/Job.kt
💤 Files with no reviewable changes (2)
- core/network/src/main/java/kr/co/call/network/util/SafeApiCall.kt
- core/domain/src/main/java/kr/co/call/domain/model/onboarding/OnboardingInput.kt
🚧 Files skipped from review as they are similar to previous changes (4)
- feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/model/Job.kt
- core/network/src/main/java/kr/co/call/network/api/MyPageApi.kt
- core/data/src/main/java/kr/co/call/data/repositoryImpl/MyPageRepositoryImpl.kt
- feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingViewModel.kt
| <!-- 리프레시 토큰 등 인증 정보가 클라우드 백업에 포함되면 앱 삭제 후 재설치해도 자동 로그인되는 버그 발생 --> | ||
| <exclude domain="file" path="datastore/call_from_ai_token_prefs.preferences_pb"/> |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
echo "== dataExtractionRules 참조 확인 =="
rg -n -C 3 'dataExtractionRules|fullBackupContent' -g 'AndroidManifest.xml' .
echo "== 현재 데이터 추출 규칙 확인 =="
cat -n app/src/main/res/xml/data_extraction_rules.xmlRepository: call-from-ai/CallFromAi_Android
Length of output: 1729
🌐 Web query:
Android 12 device transfer data restoration excluded backup data documentation exclude device transfer
💡 Result:
For devices running Android 12 (API level 31) and higher, the android:allowBackup and android:fullBackupContent attributes are deprecated for controlling device-to-device (D2D) transfers and now apply only to cloud-based backups [1]. To control data exclusion for both cloud backups and D2D transfers on Android 12+, you must use the android:dataExtractionRules attribute in your AndroidManifest.xml [2][3][4]. Configuration Procedure 1. Update AndroidManifest.xml: Add the android:dataExtractionRules attribute pointing to an XML resource file (e.g., @xml/data_extraction_rules) [3][5]. <application... android:dataExtractionRules="@xml/data_extraction_rules"> 2. Create the Rules File: Define your inclusion and exclusion rules within the res/xml/data_extraction_rules.xml file [3][5]. This file allows you to define rules separately for cloud-backup and device-transfer [3][4]. Example Configuration: Key Considerations * D2D Transfers: If the android:dataExtractionRules attribute is not specified, the system defaults to transferring all application data during a D2D migration [2]. * Backward Compatibility: If your app supports Android 11 (API level 30) or lower, you must still maintain android:fullBackupContent and its associated backup rules file to ensure consistent behavior across all supported Android versions [2][1][5]. * Granular Control: You can use the include and exclude tags within both cloud-backup and device-transfer blocks to specify which domains (e.g., file, database, sharedpref, external, root) and specific file paths to include or exclude [3][4]. Note that if you use an include tag, the system will only back up or transfer the specified files, ignoring others by default [6]. For further guidance, consult the official Android documentation on Backup Best Practices and Auto Backup.
Citations:
- 1: https://googlesamples.github.io/android-custom-lint-rules/checks/DataExtractionRules.md.html
- 2: https://developer.android.com/privacy-and-security/risks/backup-best-practices
- 3: https://developer.android.com/identity/data/autobackup
- 4: https://mas.owasp.org/MASTG/tests/android/MASVS-STORAGE/MASTG-TEST-0262/
- 5: https://stackoverflow.com/questions/70365809/how-to-specify-to-not-allow-any-data-backup-with-androiddataextractionrules
- 6: https://stackoverflow.com/questions/57911803/what-is-the-correct-way-to-exclude-a-selected-preference-file-from-full-backup
🌐 Web query:
Android default device transfer includes all app data unless data extraction rules exclude
💡 Result:
The statement is generally correct regarding how Android handles app data during migration. By default, the Android system includes application data in both cloud backups and device-to-device (D2D) transfers unless the application developer explicitly excludes it using configuration rules [1][2]. Key details regarding this process include: 1. Automatic Inclusion: For most apps targeting Android 6.0 (API level 23) or higher, Auto Backup is enabled by default [2]. If no specific backup or data extraction rules are defined, the system will attempt to transfer the app's data during a device migration [1]. 2. Configuration Rules: Developers can control this behavior using XML configuration files. - On Android 12 (API level 31) and higher, developers use the android:dataExtractionRules attribute in the AndroidManifest.xml to point to a configuration file that defines and rules [3][4]. - If the tag is not configured or does not include an explicit exclusion, the default behavior remains to transfer the application's data [1]. - Developers can use these rules to include or exclude specific domains (such as file, database, sharedpref, external, or root) and paths [3][5]. 3. Limitations: Not all app data is guaranteed to transfer [6][7]. The success of a transfer depends on: - The application developer's implementation: If an app does not use the standard Android backup framework or has explicitly excluded its data, that data will not be transferred [6][2]. - System limitations: Certain types of data, such as app data for apps not from the Google Play Store or data from apps that do not utilize Android backup, may not be included [6]. - Version compatibility: Rules for data extraction were refined in Android 12, requiring developers to ensure their backup configurations are compatible across different Android versions [1][4]. In summary, while the system defaults to including app data, developers retain the authority to exclude specific data or prevent it from being backed up/transferred entirely through defined extraction rules [1][4]. Users should be aware that the actual data transferred can vary by app [6][7].
Citations:
- 1: https://developer.android.com/privacy-and-security/risks/backup-best-practices
- 2: https://developer.android.com/identity/data/backup
- 3: https://developer.android.com/identity/data/autobackup
- 4: https://stackoverflow.com/questions/70365809/how-to-specify-to-not-allow-any-data-backup-with-androiddataextractionrules
- 5: https://stackoverflow.com/questions/76574156/android-auto-backup-xml-rules
- 6: https://support.google.com/android/answer/13761358?hl=en-GB
- 7: https://support.google.com/android/answer/2819582
🌐 Web query:
Android BackupDataTransport DeviceShadowFileAgent device transfer fullBackupContent all app data without data extraction rules
💡 Result:
The terms you mentioned relate to different layers and concepts within the Android backup and data transfer infrastructure: BackupDataTransport is not a standard public API class but rather refers to the underlying transport mechanism (often implementing the IBackupTransport interface) that handles the movement of backup data to storage or between devices [1][2][3]. DeviceShadowFileAgent is not a documented Android system class or standard API component. It is possible this is an internal identifier or a misunderstanding of how the system "shadows" or handles file-based backups during data migration. FullBackupContent refers to the android:fullBackupContent attribute in an app's AndroidManifest.xml [4][5]. This attribute allows developers to point to an XML resource file that defines rules for including or excluding specific files and directories during Auto Backup and device-to-device transfers [6][4][7]. When an app does not define data extraction rules (or an explicit fullBackupContent/dataExtractionRules configuration), the Android system's default behavior is to include almost all data within the application's local directory (such as /data/data/<package_name>/) in the backup set, provided that android:allowBackup is set to true (or is left as the default true) [4][5]. In the context of device-to-device transfers, the system uses these same rules. If no specific exclusions are defined, the backup agent—which is responsible for the interaction between the application and the backup system—will process all accessible data files unless otherwise restricted by the system's own security policies [8][6]. Because backup and restoration involve an app's private files, failure to explicitly define these rules can lead to the accidental inclusion of sensitive information (like tokens, cache, or device-specific identifiers) in transfers to other devices [4][7].
Citations:
- 1: https://android.googlesource.com/platform/frameworks/base/+/HEAD/core/java/android/app/backup/BackupTransport.java
- 2: https://android.googlesource.com/platform/frameworks/base.git/+/master/core/java/com/android/internal/backup/IBackupTransport.aidl
- 3: https://android.googlesource.com/platform/frameworks/base/+/master/packages/LocalTransport/src/com/android/localtransport/LocalTransport.java
- 4: https://mas.owasp.org/MASTG/tests/android/MASVS-STORAGE/MASTG-TEST-0262/
- 5: https://nirajkharel.com.np/posts/allow-backup-data-extraction/
- 6: https://developer.android.com/identity/data/autobackup
- 7: https://stackoverflow.com/questions/30804168/what-does-it-mean-should-explicitly-set-androidfullbackupcontent-to-avoid-ba
- 8: https://developer.android.com/reference/android/app/backup/BackupAgent
🌐 Web query:
Android frameworks base BackupManagerService shouldIncludeDeviceTransfer device shadow file device transfer not including all app data
💡 Result:
In the Android backup framework, there is no public or standard internal method named "shouldIncludeDeviceTransfer" within the BackupManagerService or related classes. The mechanism for determining what data is included in a device-to-device (D2D) transfer or cloud backup is primarily controlled through the application's backup configuration, specifically the backup_rules.xml file [1][2]. If an application is not including all expected app data during a device transfer, it is typically due to one of the following factors rather than a framework-level "shadow file" issue: 1. Configuration Rules (backup_rules.xml): Developers define which data to include or exclude for cloud backups and device-to-device transfers using the and tags in their configuration XML [1]. If data is missing, it is often because it falls under an excluded domain or path, or because the specific rules were not correctly configured to include the desired data [1][2]. 2. Transport Capabilities: Device-to-device transfer uses a specific transport mechanism (often the D2dTransport) [3][4]. Not all app data is guaranteed to be transferred if it does not meet the requirements of this transport or if the app has opted out of certain types of backups [1][2]. 3. Security and Encryption: Android may exclude data from backups—especially during automated processes—if the data is marked as sensitive or if the device does not meet specific security requirements (such as having a screen lock set for client-side encryption) [1][2]. 4. Backup Agent Implementation: Applications can override default behavior by implementing a BackupAgent [5]. If the agent is not correctly configured to handle full-backup or key-value data, or if it explicitly excludes files during the backup process, the data will not be available for transfer [6][5]. To debug issues where app data is missing after a transfer, developers should verify their backup_rules.xml to ensure proper inclusion of required domains (such as device_file, device_database, or root) [1] and confirm that the BackupAgent is not filtering out the files during the traversal of the backup file tree [6]. You can test this behavior by using the bmgr tool to simulate backups and inspect what data is being handled by the active transport [3][4].
Citations:
- 1: https://developer.android.com/identity/data/autobackup
- 2: https://developer.android.com/privacy-and-security/risks/backup-best-practices
- 3: https://developer.android.google.cn/identity/data/testingbackup
- 4: https://developer.android.com/identity/data/testingbackup
- 5: https://developer.android.com/reference/android/app/backup/BackupManager
- 6: https://android.googlesource.com/platform/frameworks/base/+/HEAD/core/java/android/app/backup/BackupAgent.java
device-transfer에서도 토큰 DataStore를 제외하세요.
android:dataExtractionRules="@xml/data_extraction_rules"은 현재 cloud-backup만 적용합니다. device-transfer 규칙은 비활성 상태라 Android 12 이상 기기 간 전송 경로에서 토큰 파일이 포함될 수 있습니다. device-transfer를 활성화하고 datastore/call_from_ai_token_prefs.preferences_pb도 제외하세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/src/main/res/xml/data_extraction_rules.xml` around lines 12 - 13, Update
the data extraction rules to enable the device-transfer configuration and
exclude datastore/call_from_ai_token_prefs.preferences_pb there as well as in
the existing cloud-backup rules, preserving the token exclusion for both backup
paths.
Gemini AI 코드리뷰안녕하세요, 시니어 Android 개발자입니다. PR 리뷰 요청해주셔서 감사합니다. diff 내용을 꼼꼼히 살펴보았으니, 아래 피드백을 참고하여 더욱 견고하고 유지보수하기 쉬운 코드를 만들어나가시길 바랍니다. 전반적인 평가이번 PR은 온보딩 플로우 구현과 기존 로그인/로그아웃/회원탈퇴 로직 개선, 그리고 세션 만료 시 전역적인 로그아웃 처리를 위한 중요한 변경사항들을 담고 있습니다. Orbit MVI 패턴과 몇 가지 개선할 점과 코드 리뷰 상세1. Kotlin 코드
2. Jetpack Compose 코드
3. Repository/DataSource 레이어
4. ViewModel
5. FCM/SSE/실시간 통신 관련 코드
전반적인 개선 제안
결론이번 PR은 전반적으로 매우 높은 수준의 코드 품질과 아키텍처 준수율을 보여줍니다. 특히 Orbit MVI 패턴의 올바른 사용, 계속해서 좋은 코드 부탁드립니다! |
JiwonLee42
left a comment
There was a problem hiding this comment.
ViewModel 코드 나중에 꼭 수정하기!!
📄 작업 내용 요약
-로그아웃, 회원탈퇴 api연결
📎 Issue 번호
✅ 작업 목록
📝 기타 참고사항
Summary by CodeRabbit
새로운 기능
버그 수정