feat: 설정 화면 구현 (프로필 수정, 로그아웃, 회원 탈퇴)#210
Hidden character warning
Conversation
- EditProfileScreen: 프로필 이미지 변경, 닉네임 중복 체크 후 수정 완료 - WithdrawScreen: 유의사항 확인 체크박스, Destructive 버튼 상태로 탈퇴 처리 - setting 패키지를 setting/editprofile/withdraw 로 분리하여 구조 정리 - FlintButtonState에 Destructive 상태(error500) 추가 - Route에 EditProfile, Withdraw 추가 및 네비게이션 연결 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Constants에 USER_EMAIL DataStore 키 추가 - AuthRepository에 logout() 함수 추가 - FlintBasicButton/FlintMediumButton에 textStyle 파라미터 추가 - ProfileScreen/ProfileNavigation에 navigateToSetting 연결 - ic_kakao_full 드로어블 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
# Conflicts: # app/src/main/java/com/flint/domain/repository/AuthRepository.kt
- 프로필 이미지 변경: Presigned URL → S3 업로드 → PUT /api/v1/users/me/profile-image - 닉네임 수정: PUT /api/v1/users/me/nickname API 연결 및 로컬 저장소 동기화 - 자음/모음 단독 입력 시 확인/완료 버튼 비활성화 (온보딩과 동일한 로직) - 닉네임 정규식 자모 중간 입력 허용으로 수정 - 토스트 로직 온보딩 프로필 화면과 동일하게 통일 - AuthRepository logout() 추가 - data 필드 없는 응답 처리를 위한 BaseEmptyResponse 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
피그마 디자인에 맞춰 로그아웃 확인 다이얼로그를 TwoButtonModal 컴포넌트로 교체하고, 개인정보 정책/이용약관 메뉴에 외부 링크 연결, 탈퇴 화면에 시스템 바 패딩을 적용했다. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
탈퇴 성공 시 로그인 화면으로 바로 이동하는 대신 '회원 탈퇴 완료' 화면을 거쳐 첫 화면으로 이동하도록 변경 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Warning Review limit reached
More reviews will be available in 15 minutes and 34 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more credits in the billing tab to continue. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (7)
📝 Walkthrough워크스루설정, 프로필 수정, 계정 탈퇴 기능을 지원하기 위해 네비게이션 라우트 4개를 추가하고, API 레이어에 사용자 정보 업데이트 엔드포인트를 추가하며, 4개의 새로운 Compose 화면(설정, 프로필 수정, 탈퇴, 탈퇴 완료)을 구현합니다. 변경 사항설정, 프로필 수정, 탈퇴 기능
가능성 있는 관련 PR
추천 라벨
추천 리뷰어
시 (Poem)
🎯 4 (Complex) | ⏱️ ~60 minutes 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 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: 7
🧹 Nitpick comments (4)
app/src/main/java/com/flint/presentation/setting/SettingScreen.kt (2)
128-128: ⚡ Quick win하드코딩된 URL을 상수로 추출하는 것을 권장합니다.
개인정보 정책과 이용약관 URL이 하드코딩되어 있습니다. URL이 변경되면 여러 곳을 수정해야 하고, 테스트나 환경별 설정이 어렵습니다.
Constants.kt또는BuildConfig에 URL을 정의하는 것을 고려하세요.♻️ 권장 리팩토링
Constants.kt에 URL 상수 추가:object ExternalLinks { const val PRIVACY_POLICY_URL = "https://artistic-bacon-a40.notion.site/35650cdb714e804cb642eb2cc576a62c" const val TERMS_OF_SERVICE_URL = "https://artistic-bacon-a40.notion.site/35650cdb714e8054a69bcbd110ed19dd" }사용:
SettingMenuItem( label = "개인정보 정책", onClick = { context.startActivity( - Intent(Intent.ACTION_VIEW, Uri.parse("https://artistic-bacon-a40.notion.site/35650cdb714e804cb642eb2cc576a62c")) + Intent(Intent.ACTION_VIEW, Uri.parse(ExternalLinks.PRIVACY_POLICY_URL)) ) }, )Also applies to: 139-139
🤖 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/java/com/flint/presentation/setting/SettingScreen.kt` at line 128, The hardcoded Notion URLs in SettingScreen (the Intent creation at Intent(Intent.ACTION_VIEW, Uri.parse(...)) lines) should be extracted into named constants; create an object (e.g., ExternalLinks with PRIVACY_POLICY_URL and TERMS_OF_SERVICE_URL) in a shared Constants.kt (or add them to BuildConfig) and replace the Uri.parse(...) literals in SettingScreen with ExternalLinks.PRIVACY_POLICY_URL and ExternalLinks.TERMS_OF_SERVICE_URL respectively so all references use the single source of truth and are easier to test and change.
232-232: ⚡ Quick win매직 스트링 비교 대신 파라미터를 사용하세요.
라벨 텍스트로 특정 패딩을 결정하는 로직(
if(label == "이용약관"))은 취약합니다. 라벨이 변경되거나 다국어화되면 작동하지 않습니다.패딩을 별도 파라미터로 받거나, 서로 다른 컴포저블 변형을 사용하는 것이 더 안전합니다.
♻️ 권장 리팩토링
`@Composable` private fun SettingMenuItem( label: String, modifier: Modifier = Modifier, + verticalPadding: Dp = 18.dp, onClick: () -> Unit = {}, trailingContent: `@Composable` () -> Unit = {}, ) { Row( modifier = modifier .fillMaxWidth() .noRippleClickable(onClick = onClick) - .padding(horizontal = 20.dp, vertical = if(label == "이용약관") 25.dp else 18.dp), + .padding(horizontal = 20.dp, vertical = verticalPadding), verticalAlignment = Alignment.CenterVertically, ) {사용:
SettingMenuItem( label = "이용약관", + verticalPadding = 25.dp, onClick = { ... }, )🤖 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/java/com/flint/presentation/setting/SettingScreen.kt` at line 232, The composable currently uses a magic-string check (if (label == "이용약관")) to choose vertical padding which breaks on text changes or i18n; update the API to accept an explicit padding parameter (e.g., extraVerticalPadding: Dp or contentVerticalPadding: Dp) or a boolean flag (e.g., isTerms: Boolean) on the composable (the function rendering the setting row in SettingScreen.kt) and use that parameter in the .padding call instead of comparing label, then update all callers of that composable to pass the appropriate padding/flag.app/src/main/java/com/flint/domain/repository/UserRepository.kt (1)
74-74: 💤 Low value명시적
Unit반환은 불필요합니다.Kotlin에서
Unit을 반환하는 함수는 명시적으로Unit을 반환할 필요가 없습니다.suspendRunCatching블록의 마지막 표현식이 API 호출이고, 이는BaseEmptyResponse를 반환하므로 명시적Unit이 필요하지만, 더 간결하게 API 호출 결과를 무시하고 암시적으로Unit을 반환하도록 할 수 있습니다.♻️ 선택적 리팩토링 제안
suspend fun updateNickname(nickname: String): Result<Unit> = suspendRunCatching { apiService.updateNickname(UpdateNicknameRequestDto(nickname = nickname)) - Unit }suspend fun updateProfileImage(imageKey: String): Result<Unit> = suspendRunCatching { apiService.updateProfileImage(UpdateProfileImageRequestDto(profileImage = imageKey)) - Unit }Also applies to: 81-81
🤖 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/java/com/flint/domain/repository/UserRepository.kt` at line 74, Remove the explicit "Unit" return inside the suspendRunCatching block in UserRepository.kt: instead of returning Unit after the API call that yields a BaseEmptyResponse, simply let the API call be the last expression and ignore its result so the block returns Unit implicitly; update both occurrences flagged (around the suspendRunCatching usages) to drop the explicit Unit and rely on the API call/BaseEmptyResponse being discarded for an implicit Unit result.app/src/main/java/com/flint/presentation/setting/SettingViewModel.kt (1)
35-49: 💤 Low value두 개의 독립적인 코루틴을 하나로 결합할 수 있습니다.
Line 36과 Line 42에서 각각 별도의
viewModelScope.launch를 사용하고 있습니다. 두 작업이 독립적이라면 현재 구조가 괜찮지만, DataStore 읽기 후 API 호출 순서를 보장하거나 단일 try-catch로 에러를 처리하려면 하나의 코루틴으로 결합하는 것이 더 명확할 수 있습니다.선택적 리팩토링 예시
fun loadUserInfo() { viewModelScope.launch { - val nickname = preferencesManager.getString(USER_NAME).first() - val email = preferencesManager.getString(USER_EMAIL).first() - _uiState.update { it.copy(nickname = nickname, email = email) } - } - - viewModelScope.launch { + val nickname = preferencesManager.getString(USER_NAME).first() + val email = preferencesManager.getString(USER_EMAIL).first() + _uiState.update { it.copy(nickname = nickname, email = email) } + userRepository.getUserProfile(userId = null) .onSuccess { profile -> _uiState.update { it.copy(profileImageUrl = profile.profileImageUrl) } } .onFailure { Timber.e(it) } } }🤖 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/java/com/flint/presentation/setting/SettingViewModel.kt` around lines 35 - 49, The loadUserInfo function launches two separate coroutines; combine them into a single viewModelScope.launch so the DataStore reads (preferencesManager.getString(USER_NAME).first(), USER_EMAIL) and the network call (userRepository.getUserProfile(userId = null)) execute in one coroutine and can share error handling and ordering. Inside that single launch, read nickname/email, update _uiState with them, then call getUserProfile and update profileImageUrl; wrap the network call (or the whole sequence) in a try/catch or use runCatching to handle errors and call Timber.e on failure. Ensure you keep calls to _uiState.update to set nickname/email and profileImageUrl in the combined flow.
🤖 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/java/com/flint/presentation/setting/editprofile/EditProfileUiState.kt`:
- Line 25: isLengthValid currently uses nickname.length (UTF-16 code unit count)
which differs from the grapheme-based length shown in EditProfileScreen; update
the validation to use graphemeLength for nickname (e.g., replace nickname.length
with nickname.graphemeLength) so isLengthValid, MIN_LENGTH check, and the UI
display (EditProfileScreen) stay consistent; adjust any imports or utility
references needed to access graphemeLength where isLengthValid is declared.
In
`@app/src/main/java/com/flint/presentation/setting/editprofile/EditProfileViewModel.kt`:
- Around line 42-55: In loadProfile(), the DataStore read via
preferencesManager.getString(USER_NAME).first() can throw but has no error
handling; wrap that read in a try/catch (or use runCatching/catch on the Flow)
inside the viewModelScope.launch so failures are logged (e.g., Timber.e) and the
_uiState.update still uses a safe fallback (keep existing nickname or empty
string) instead of crashing; target the loadProfile function and the block that
calls preferencesManager.getString(USER_NAME).first() and ensure any exception
updates/logs appropriately before proceeding to the userRepository
getUserProfile flow.
- Around line 104-119: saveProfile() currently calls uploadProfileImage() and
always emits _navigateUp even if uploadProfileImage fails and there is no
loading/error UI state; change uploadProfileImage(...) to return a Result (or
throw/catch and map to Result) and update _uiState to include loading and error
fields, set loading = true at start of viewModelScope.launch and false on
completion, handle uploadProfileImage() failure by setting an error in _uiState
and aborting navigation, and only proceed to call
userRepository.updateNickname(...) and emit _navigateUp when both image upload
(uploadProfileImage) and nickname update succeed; also surface nickname update
failures into _uiState.error for user feedback.
In `@app/src/main/java/com/flint/presentation/setting/SettingScreen.kt`:
- Around line 127-130: Wrap calls that launch external URLs using
Intent(Intent.ACTION_VIEW, Uri.parse(...)) and context.startActivity(...) in a
try-catch that catches android.content.ActivityNotFoundException; in the catch,
show a user-friendly fallback (e.g., a Toast or Snackbar) informing the user
that no app can open the link. Add the import for ActivityNotFoundException if
needed and apply the same change to both occurrences around the two
Intent.ACTION_VIEW usages in SettingScreen (the blocks shown at the current and
lines 138-141).
In `@app/src/main/java/com/flint/presentation/setting/SettingUiState.kt`:
- Line 8: The isWithdrawDialogVisible flag and its
showWithdrawDialog()/dismissWithdrawDialog() exist only in
SettingUiState/SettingViewModel but are unused by SettingScreen (which currently
only calls navigateToWithdraw); either wire the confirm dialog in SettingScreen
to uiState.isWithdrawDialogVisible (render an AlertDialog when
SettingUiState.isWithdrawDialogVisible is true, call
SettingViewModel.showWithdrawDialog() from the “탈퇴하기” click and
SettingViewModel.dismissWithdrawDialog() on cancel/confirm, and call
navigateToWithdraw() only after confirm) or remove the flag and the two methods
from SettingUiState/SettingViewModel (and any tests) to keep code tidy—pick one
approach and implement the corresponding changes across SettingScreen,
SettingViewModel, and SettingUiState so there are no unused members.
- Line 6: SettingUiState exposes email and isWithdrawDialogVisible but
SettingScreen never uses them; either remove these unused fields from
SettingUiState and the toggle logic in SettingViewModel, or wire them into the
UI: have SettingScreen read uiState.email and render it where the email should
appear, and observe uiState.isWithdrawDialogVisible to show/hide the withdraw
confirmation dialog (AlertDialog) and hook dialog actions to the ViewModel's
withdraw/close methods (e.g., call the existing toggle/confirm/cancel methods on
SettingViewModel). Ensure references to SettingUiState.email and
SettingUiState.isWithdrawDialogVisible are added in SettingScreen and
corresponding ViewModel handlers are invoked for dialog actions.
In `@app/src/main/java/com/flint/presentation/setting/SettingViewModel.kt`:
- Around line 35-49: The DataStore reads in loadUserInfo using
preferencesManager.getString(USER_NAME) and getString(USER_EMAIL) lack error
handling; wrap those reads in a try/catch or use runCatching when retrieving
nickname/email inside the viewModelScope.launch, log failures with Timber.e, and
update _uiState with safe defaults or an error flag so the UI can show feedback
instead of silently showing empty values; ensure the changes reference
loadUserInfo, preferencesManager.getString, USER_NAME, USER_EMAIL, _uiState and
viewModelScope.launch.
---
Nitpick comments:
In `@app/src/main/java/com/flint/domain/repository/UserRepository.kt`:
- Line 74: Remove the explicit "Unit" return inside the suspendRunCatching block
in UserRepository.kt: instead of returning Unit after the API call that yields a
BaseEmptyResponse, simply let the API call be the last expression and ignore its
result so the block returns Unit implicitly; update both occurrences flagged
(around the suspendRunCatching usages) to drop the explicit Unit and rely on the
API call/BaseEmptyResponse being discarded for an implicit Unit result.
In `@app/src/main/java/com/flint/presentation/setting/SettingScreen.kt`:
- Line 128: The hardcoded Notion URLs in SettingScreen (the Intent creation at
Intent(Intent.ACTION_VIEW, Uri.parse(...)) lines) should be extracted into named
constants; create an object (e.g., ExternalLinks with PRIVACY_POLICY_URL and
TERMS_OF_SERVICE_URL) in a shared Constants.kt (or add them to BuildConfig) and
replace the Uri.parse(...) literals in SettingScreen with
ExternalLinks.PRIVACY_POLICY_URL and ExternalLinks.TERMS_OF_SERVICE_URL
respectively so all references use the single source of truth and are easier to
test and change.
- Line 232: The composable currently uses a magic-string check (if (label ==
"이용약관")) to choose vertical padding which breaks on text changes or i18n; update
the API to accept an explicit padding parameter (e.g., extraVerticalPadding: Dp
or contentVerticalPadding: Dp) or a boolean flag (e.g., isTerms: Boolean) on the
composable (the function rendering the setting row in SettingScreen.kt) and use
that parameter in the .padding call instead of comparing label, then update all
callers of that composable to pass the appropriate padding/flag.
In `@app/src/main/java/com/flint/presentation/setting/SettingViewModel.kt`:
- Around line 35-49: The loadUserInfo function launches two separate coroutines;
combine them into a single viewModelScope.launch so the DataStore reads
(preferencesManager.getString(USER_NAME).first(), USER_EMAIL) and the network
call (userRepository.getUserProfile(userId = null)) execute in one coroutine and
can share error handling and ordering. Inside that single launch, read
nickname/email, update _uiState with them, then call getUserProfile and update
profileImageUrl; wrap the network call (or the whole sequence) in a try/catch or
use runCatching to handle errors and call Timber.e on failure. Ensure you keep
calls to _uiState.update to set nickname/email and profileImageUrl in the
combined flow.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: cceae8c2-66c5-4c1a-801f-b58c486aed51
⛔ Files ignored due to path filters (1)
app/src/main/res/drawable/ic_kakao_full.pngis excluded by!**/*.png
📒 Files selected for processing (28)
app/src/main/java/com/flint/core/common/util/Constants.ktapp/src/main/java/com/flint/core/designsystem/component/button/FlintBasicButton.ktapp/src/main/java/com/flint/core/designsystem/component/button/FlintButtonState.ktapp/src/main/java/com/flint/core/designsystem/component/button/FlintMediumButton.ktapp/src/main/java/com/flint/core/navigation/Route.ktapp/src/main/java/com/flint/data/api/UserApi.ktapp/src/main/java/com/flint/data/dto/base/BaseEmptyResponse.ktapp/src/main/java/com/flint/data/dto/user/request/UpdateNicknameRequestDto.ktapp/src/main/java/com/flint/data/dto/user/request/UpdateProfileImageRequestDto.ktapp/src/main/java/com/flint/domain/repository/AuthRepository.ktapp/src/main/java/com/flint/domain/repository/UserRepository.ktapp/src/main/java/com/flint/presentation/main/MainNavHost.ktapp/src/main/java/com/flint/presentation/main/MainNavigator.ktapp/src/main/java/com/flint/presentation/profile/ProfileScreen.ktapp/src/main/java/com/flint/presentation/profile/navigation/ProfileNavigation.ktapp/src/main/java/com/flint/presentation/setting/SettingScreen.ktapp/src/main/java/com/flint/presentation/setting/SettingUiState.ktapp/src/main/java/com/flint/presentation/setting/SettingViewModel.ktapp/src/main/java/com/flint/presentation/setting/editprofile/EditProfileScreen.ktapp/src/main/java/com/flint/presentation/setting/editprofile/EditProfileUiState.ktapp/src/main/java/com/flint/presentation/setting/editprofile/EditProfileViewModel.ktapp/src/main/java/com/flint/presentation/setting/editprofile/navigation/EditProfileNavigation.ktapp/src/main/java/com/flint/presentation/setting/navigation/SettingNavigation.ktapp/src/main/java/com/flint/presentation/setting/withdraw/WithdrawCompleteScreen.ktapp/src/main/java/com/flint/presentation/setting/withdraw/WithdrawScreen.ktapp/src/main/java/com/flint/presentation/setting/withdraw/WithdrawViewModel.ktapp/src/main/java/com/flint/presentation/setting/withdraw/navigation/WithdrawCompleteNavigation.ktapp/src/main/java/com/flint/presentation/setting/withdraw/navigation/WithdrawNavigation.kt
| } | ||
|
|
||
| val isNicknameChanged: Boolean get() = nickname != initialNickname | ||
| val isLengthValid: Boolean get() = nickname.length >= MIN_LENGTH |
There was a problem hiding this comment.
길이 검증이 UI 표시와 일관성이 부족합니다.
Line 25의 isLengthValid는 nickname.length(UTF-16 코드 유닛 수)를 사용하지만, EditProfileScreen Line 169에서는 graphemeLength로 사용자에게 표시합니다. 현재 정규식(Line 17)은 한글과 ASCII만 허용하므로 실제로는 문제가 없지만, 향후 정규식이 변경되거나 이모지를 허용하게 되면 불일치가 발생할 수 있습니다.
일관성 개선 방안
Pebble 라이브러리의 graphemeLength를 사용하여 길이 검증도 통일:
+import com.chattymin.pebble.graphemeLength
+
data class EditProfileUiState(
...
) {
...
- val isLengthValid: Boolean get() = nickname.length >= MIN_LENGTH
+ val isLengthValid: Boolean get() = nickname.graphemeLength >= MIN_LENGTH🤖 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/java/com/flint/presentation/setting/editprofile/EditProfileUiState.kt`
at line 25, isLengthValid currently uses nickname.length (UTF-16 code unit
count) which differs from the grapheme-based length shown in EditProfileScreen;
update the validation to use graphemeLength for nickname (e.g., replace
nickname.length with nickname.graphemeLength) so isLengthValid, MIN_LENGTH
check, and the UI display (EditProfileScreen) stay consistent; adjust any
imports or utility references needed to access graphemeLength where
isLengthValid is declared.
| fun loadProfile() { | ||
| viewModelScope.launch { | ||
| val nickname = preferencesManager.getString(USER_NAME).first() | ||
| _uiState.update { it.copy(initialNickname = nickname, nickname = nickname) } | ||
| } | ||
|
|
||
| viewModelScope.launch { | ||
| userRepository.getUserProfile(userId = null) | ||
| .onSuccess { profile -> | ||
| _uiState.update { it.copy(profileImageUrl = profile.profileImageUrl) } | ||
| } | ||
| .onFailure { Timber.e(it) } | ||
| } | ||
| } |
There was a problem hiding this comment.
DataStore 읽기 실패 시 에러 처리가 누락되었습니다.
Line 44에서 DataStore의 getString() 호출은 실패할 수 있지만 에러 처리가 없습니다. SettingViewModel과 동일한 이슈입니다.
제안하는 에러 처리 방법
fun loadProfile() {
viewModelScope.launch {
- val nickname = preferencesManager.getString(USER_NAME).first()
- _uiState.update { it.copy(initialNickname = nickname, nickname = nickname) }
+ runCatching {
+ val nickname = preferencesManager.getString(USER_NAME).first()
+ _uiState.update { it.copy(initialNickname = nickname, nickname = nickname) }
+ }.onFailure { Timber.e(it, "Failed to load nickname from 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
`@app/src/main/java/com/flint/presentation/setting/editprofile/EditProfileViewModel.kt`
around lines 42 - 55, In loadProfile(), the DataStore read via
preferencesManager.getString(USER_NAME).first() can throw but has no error
handling; wrap that read in a try/catch (or use runCatching/catch on the Flow)
inside the viewModelScope.launch so failures are logged (e.g., Timber.e) and the
_uiState.update still uses a safe fallback (keep existing nickname or empty
string) instead of crashing; target the loadProfile function and the block that
calls preferencesManager.getString(USER_NAME).first() and ensure any exception
updates/logs appropriately before proceeding to the userRepository
getUserProfile flow.
| context.startActivity( | ||
| Intent(Intent.ACTION_VIEW, Uri.parse("https://artistic-bacon-a40.notion.site/35650cdb714e804cb642eb2cc576a62c")) | ||
| ) | ||
| }, |
There was a problem hiding this comment.
외부 링크 열기 시 예외 처리 추가 권장.
Intent.ACTION_VIEW로 URL을 열 때 브라우저가 설치되지 않은 경우 ActivityNotFoundException이 발생할 수 있습니다. 드문 경우지만, 예외 처리를 추가하면 사용자 경험이 개선됩니다.
🛡️ 권장 수정
SettingMenuItem(
label = "개인정보 정책",
onClick = {
- context.startActivity(
- Intent(Intent.ACTION_VIEW, Uri.parse("https://artistic-bacon-a40.notion.site/35650cdb714e804cb642eb2cc576a62c"))
- )
+ try {
+ context.startActivity(
+ Intent(Intent.ACTION_VIEW, Uri.parse("https://artistic-bacon-a40.notion.site/35650cdb714e804cb642eb2cc576a62c"))
+ )
+ } catch (e: ActivityNotFoundException) {
+ // 토스트 메시지 또는 스낵바로 사용자에게 알림
+ }
},
)Also applies to: 138-141
🤖 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/java/com/flint/presentation/setting/SettingScreen.kt` around
lines 127 - 130, Wrap calls that launch external URLs using
Intent(Intent.ACTION_VIEW, Uri.parse(...)) and context.startActivity(...) in a
try-catch that catches android.content.ActivityNotFoundException; in the catch,
show a user-friendly fallback (e.g., a Toast or Snackbar) informing the user
that no app can open the link. Add the import for ActivityNotFoundException if
needed and apply the same change to both occurrences around the two
Intent.ACTION_VIEW usages in SettingScreen (the blocks shown at the current and
lines 138-141).
| fun loadUserInfo() { | ||
| viewModelScope.launch { | ||
| val nickname = preferencesManager.getString(USER_NAME).first() | ||
| val email = preferencesManager.getString(USER_EMAIL).first() | ||
| _uiState.update { it.copy(nickname = nickname, email = email) } | ||
| } | ||
|
|
||
| viewModelScope.launch { | ||
| userRepository.getUserProfile(userId = null) | ||
| .onSuccess { profile -> | ||
| _uiState.update { it.copy(profileImageUrl = profile.profileImageUrl) } | ||
| } | ||
| .onFailure { Timber.e(it) } | ||
| } | ||
| } |
There was a problem hiding this comment.
DataStore 읽기 실패 시 에러 처리가 누락되었습니다.
Line 37-39에서 DataStore의 getString() 호출은 실패할 수 있지만 에러 처리가 없습니다. 네트워크 문제나 데이터 손상 시 사용자에게 피드백 없이 빈 값이 표시될 수 있습니다.
제안하는 에러 처리 방법
fun loadUserInfo() {
viewModelScope.launch {
- val nickname = preferencesManager.getString(USER_NAME).first()
- val email = preferencesManager.getString(USER_EMAIL).first()
- _uiState.update { it.copy(nickname = nickname, email = email) }
+ runCatching {
+ val nickname = preferencesManager.getString(USER_NAME).first()
+ val email = preferencesManager.getString(USER_EMAIL).first()
+ _uiState.update { it.copy(nickname = nickname, email = email) }
+ }.onFailure { Timber.e(it, "Failed to load user info from DataStore") }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fun loadUserInfo() { | |
| viewModelScope.launch { | |
| val nickname = preferencesManager.getString(USER_NAME).first() | |
| val email = preferencesManager.getString(USER_EMAIL).first() | |
| _uiState.update { it.copy(nickname = nickname, email = email) } | |
| } | |
| viewModelScope.launch { | |
| userRepository.getUserProfile(userId = null) | |
| .onSuccess { profile -> | |
| _uiState.update { it.copy(profileImageUrl = profile.profileImageUrl) } | |
| } | |
| .onFailure { Timber.e(it) } | |
| } | |
| } | |
| fun loadUserInfo() { | |
| viewModelScope.launch { | |
| runCatching { | |
| val nickname = preferencesManager.getString(USER_NAME).first() | |
| val email = preferencesManager.getString(USER_EMAIL).first() | |
| _uiState.update { it.copy(nickname = nickname, email = email) } | |
| }.onFailure { Timber.e(it, "Failed to load user info from DataStore") } | |
| } | |
| viewModelScope.launch { | |
| userRepository.getUserProfile(userId = null) | |
| .onSuccess { profile -> | |
| _uiState.update { it.copy(profileImageUrl = profile.profileImageUrl) } | |
| } | |
| .onFailure { Timber.e(it) } | |
| } | |
| } |
🤖 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/java/com/flint/presentation/setting/SettingViewModel.kt` around
lines 35 - 49, The DataStore reads in loadUserInfo using
preferencesManager.getString(USER_NAME) and getString(USER_EMAIL) lack error
handling; wrap those reads in a try/catch or use runCatching when retrieving
nickname/email inside the viewModelScope.launch, log failures with Timber.e, and
update _uiState with safe defaults or an error flag so the UI can show feedback
instead of silently showing empty values; ensure the changes reference
loadUserInfo, preferencesManager.getString, USER_NAME, USER_EMAIL, _uiState and
viewModelScope.launch.
- SettingUiState에서 미사용 email/isWithdrawDialogVisible 필드 제거 - SettingViewModel에서 미사용 showWithdrawDialog/dismissWithdrawDialog/withdraw 메서드 제거 - SettingScreen URL 하드코딩 → ExternalLinks 상수로 추출 - SettingMenuItem 매직 스트링 패딩 조건 → verticalPadding 파라미터로 교체 - EditProfileViewModel 이미지 업로드 실패 시에도 화면 이탈하던 버그 수정 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ckals413
left a comment
There was a problem hiding this comment.
[P1] 저장 실패해도 성공처럼 뒤로 이동합니다.
EditProfileViewModel.kt (line 108)
saveProfile()에서 이미지 업로드/프로필 이미지 API/닉네임 API 실패가 모두 로그만 남기고, 마지막에 항상 _navigateUp.emit(Unit)이 실행됩니다. 특히 uploadProfileImage() 내부 getOrElse { return }은 해당 함수만 빠져나오므로 호출자는 실패를 알 수 없습니다. 네트워크 실패 시 사용자는 저장된 줄 알고 이전 화면으로 돌아갑니다. uploadProfileImage()가 Result을 반환하게 하고, 모든 변경 요청이 성공했을 때만 navigate 하는 흐름이 필요합니다.
[P1] “프로필 사진 삭제” 메뉴가 실제로 아무 것도 삭제하지 못합니다.
EditProfileScreen.kt (line 227), EditProfileScreen.kt (line 140), EditProfileViewModel.kt (line 109)
삭제 클릭은 onProfileImageSelected(null)만 호출합니다. 그런데 렌더링은 profileImageUri ?: profileImageUrl이라 기존 이미지가 그대로 보이고, 저장 시에도 profileImageUri != null일 때만 API를 호출합니다. 즉 삭제 UI는 있지만 서버 반영도, 화면 반영도 되지 않습니다. “삭제 상태”를 별도 플래그로 들고 서버에 빈 키/null 삭제 요청을 보내는 식의 처리가 필요합니다.
- isProfileImageDeleted 플래그를 UiState에 추가하여 삭제 상태를 명시적으로 추적
- updateProfileImage(null) 호출 시 삭제 플래그 설정, uri 선택 시 플래그 해제
- 렌더링: 삭제 플래그가 true이면 기존 URL로 폴백하지 않고 빈 이미지 표시
- saveProfile(): 삭제 상태이면 updateProfileImage("") 호출로 서버에 반영
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
Test plan
🤖 Generated with Claude Code
Summary by CodeRabbit
릴리스 노트
새로운 기능
UI/UX 개선