Skip to content

feat: 설정 화면 구현 (프로필 수정, 로그아웃, 회원 탈퇴)#210

Merged
kimjw2003 merged 8 commits into
developfrom
FLT-19-설정화면
Jun 11, 2026

Hidden character warning

The head ref may contain hidden characters: "FLT-19-\uc124\uc815\ud654\uba74"
Merged

feat: 설정 화면 구현 (프로필 수정, 로그아웃, 회원 탈퇴)#210
kimjw2003 merged 8 commits into
developfrom
FLT-19-설정화면

Conversation

@kimjw2003

@kimjw2003 kimjw2003 commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Summary

  • 설정 화면 구현: 프로필 정보 표시, 계정/약관/정책 메뉴, 로그아웃, 탈퇴하기
  • 프로필 수정 화면 구현: 닉네임 변경(중복 확인 포함), 프로필 사진 갤러리 선택/삭제, S3 업로드
  • 탈퇴하기 화면 구현: 유의사항 확인 체크박스, 탈퇴 API 연동
  • 탈퇴 완료 화면 추가: 탈퇴 성공 후 로그인으로 바로 이동하지 않고 완료 안내 화면 경유
  • 로그아웃 다이얼로그 디자인 개선 및 개인정보 정책/이용약관 링크 연결

Test plan

  • 설정 화면 진입 시 닉네임/프로필 이미지 정상 표시 확인
  • 프로필 수정 → 닉네임 중복 확인 → 완료 저장 확인
  • 프로필 수정 → 갤러리에서 이미지 선택 후 S3 업로드 확인
  • 로그아웃 다이얼로그 → 확인 시 로그인 화면 이동 확인
  • 탈퇴하기 → 유의사항 체크 → 탈퇴 완료 화면 → 첫 화면으로 이동하기 확인

🤖 Generated with Claude Code

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 설정 화면 추가 - 사용자가 앱 설정에 접근할 수 있습니다.
    • 프로필 편집 기능 - 닉네임과 프로필 이미지를 변경할 수 있습니다.
    • 계정 탈퇴 기능 - 사용자가 계정을 삭제할 수 있습니다.
    • 로그아웃 기능 - 앱에서 로그아웃할 수 있습니다.
  • UI/UX 개선

    • 위험한 작업을 나타내는 버튼 스타일 추가

kimjw2003 and others added 6 commits May 20, 2026 22:58
- 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>
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@kimjw2003, we couldn't start this review because you've reached your PR review rate limit.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4794b8f2-c9c1-4fa8-84b7-284a8f1e9da1

📥 Commits

Reviewing files that changed from the base of the PR and between 76cf366 and 19a26b8.

📒 Files selected for processing (7)
  • app/src/main/java/com/flint/core/common/util/Constants.kt
  • app/src/main/java/com/flint/presentation/setting/SettingScreen.kt
  • app/src/main/java/com/flint/presentation/setting/SettingUiState.kt
  • app/src/main/java/com/flint/presentation/setting/SettingViewModel.kt
  • app/src/main/java/com/flint/presentation/setting/editprofile/EditProfileScreen.kt
  • app/src/main/java/com/flint/presentation/setting/editprofile/EditProfileUiState.kt
  • app/src/main/java/com/flint/presentation/setting/editprofile/EditProfileViewModel.kt
📝 Walkthrough

워크스루

설정, 프로필 수정, 계정 탈퇴 기능을 지원하기 위해 네비게이션 라우트 4개를 추가하고, API 레이어에 사용자 정보 업데이트 엔드포인트를 추가하며, 4개의 새로운 Compose 화면(설정, 프로필 수정, 탈퇴, 탈퇴 완료)을 구현합니다.

변경 사항

설정, 프로필 수정, 탈퇴 기능

레이어 / 파일(들) 요약
네비게이션 라우트 및 컴포넌트 계약
app/src/main/java/com/flint/core/navigation/Route.kt, app/src/main/java/com/flint/core/designsystem/component/button/FlintButtonState.kt, app/src/main/java/com/flint/core/common/util/Constants.kt
4개의 새로운 직렬화 가능한 라우트(Setting, EditProfile, Withdraw, WithdrawComplete)를 Route에 추가하고, FlintButtonState에 파괴적 작업용 Destructive 상태를 추가하며, DataStore에 USER_EMAIL 상수를 정의합니다.
API 레이어 및 저장소
app/src/main/java/com/flint/data/api/UserApi.kt, app/src/main/java/com/flint/data/dto/..., app/src/main/java/com/flint/domain/repository/AuthRepository.kt, app/src/main/java/com/flint/domain/repository/UserRepository.kt
닉네임 및 프로필 이미지 업데이트 API 엔드포인트 2개와 관련 요청/응답 DTO를 추가하고, AuthRepository에 logout() 메서드, UserRepository에 updateNickname()updateProfileImage() 메서드를 추가합니다.
버튼 컴포넌트 개선
app/src/main/java/com/flint/core/designsystem/component/button/FlintBasicButton.kt, app/src/main/java/com/flint/core/designsystem/component/button/FlintMediumButton.kt
FlintBasicButton에 textStyle 파라미터를 추가하고, FlintMediumButton에서 버튼 상태에 따라 텍스트 스타일을 계산한 후 전달합니다.
네비게이션 통합
app/src/main/java/com/flint/presentation/main/MainNavHost.kt, app/src/main/java/com/flint/presentation/main/MainNavigator.kt, app/src/main/java/com/flint/presentation/profile/ProfileScreen.kt, app/src/main/java/com/flint/presentation/profile/navigation/ProfileNavigation.kt
MainNavHost에 4개의 새로운 네비게이션 그래프를 등록하고, MainNavigator에 해당 메서드들을 추가하며, ProfileScreen에 설정 네비게이션 콜백을 연결합니다.
설정 화면
app/src/main/java/com/flint/presentation/setting/SettingScreen.kt, app/src/main/java/com/flint/com/flint/presentation/setting/SettingUiState.kt, app/src/main/java/com/flint/presentation/setting/SettingViewModel.kt, app/src/main/java/com/flint/presentation/setting/navigation/SettingNavigation.kt
사용자 정보, 외부 링크(정책/이용약관), 로그아웃, 탈퇴 옵션을 표시하는 설정 화면 UI, 상태 모델, ViewModel 및 네비게이션을 구현합니다. 로그아웃 확인 다이얼로그를 조건부로 표시합니다.
프로필 수정 화면
app/src/main/java/com/flint/presentation/setting/editprofile/EditProfileScreen.kt, app/src/main/java/com/flint/presentation/setting/editprofile/EditProfileUiState.kt, app/src/main/java/com/flint/presentation/setting/editprofile/EditProfileViewModel.kt, app/src/main/java/com/flint/presentation/setting/editprofile/navigation/EditProfileNavigation.kt
닉네임 입력(길이/형식 검증), 중복 확인, 프로필 이미지 선택/삭제(바텀시트), S3 업로드 기능이 있는 프로필 수정 화면, 상태 모델, ViewModel 및 네비게이션을 구현합니다.
탈퇴 화면
app/src/main/java/com/flint/presentation/setting/withdraw/WithdrawScreen.kt, app/src/main/java/com/flint/presentation/setting/withdraw/WithdrawViewModel.kt, app/src/main/java/com/flint/presentation/setting/withdraw/WithdrawCompleteScreen.kt, app/src/main/java/com/flint/presentation/setting/withdraw/navigation/...
탈퇴 유의사항 확인, 파괴적 버튼으로 탈퇴 요청, 탈퇴 완료 화면 표시, 로그인으로 리디렉트하는 전체 탈퇴 플로우를 구현합니다.

가능성 있는 관련 PR

  • imflint/Flint-Android#203: ProfileScreen의 설정 클릭 콜백(onSettingsClick / navigateToSetting)과 설정 아이콘 흐름을 추가하는 관련 변경입니다.
  • imflint/Flint-Android#208: 이 PR의 WithdrawScreen/WithdrawViewModel이 사용하는 authRepository.withdraw() 탈퇴 플로우가 해당 PR의 AuthApi/AuthRepository 탈퇴 엔드포인트 변경과 직결되어 있습니다.

추천 라벨

🧩 Component, Feat ✨, 📱 UI

추천 리뷰어

  • chanmi1125

시 (Poem)

🐰 설정 화면이 생겨나고,
프로필도 수정하고,
탈퇴 버튼도 빨갛게 빛나네요 ✨
사용자의 선택지가 늘었으니,
모두가 행복해질 거예요! 🎉


🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning PR 설명이 요약 형식으로 주요 기능(설정 화면, 프로필 수정, 탈퇴 흐름)을 명확하게 나열했으나, 템플릿의 필수 섹션과 구조가 맞지 않습니다. 템플릿 양식에 맞춰 작성하세요. 관련 이슈 번호, 스크린샷/데모 영상, 미구현 항목, 리뷰어 요청사항 등 필수 섹션을 포함하세요.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 설정 화면 구현의 핵심 기능(프로필 수정, 로그아웃, 회원 탈퇴)을 명확하게 반영하며, 변경사항의 주요 목적을 잘 요약하고 있습니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch FLT-19-설정화면

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

❤️ Share

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

@ckals413 ckals413 assigned ckals413 and kimjw2003 and unassigned ckals413 Jun 11, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 8d221be and 76cf366.

⛔ Files ignored due to path filters (1)
  • app/src/main/res/drawable/ic_kakao_full.png is excluded by !**/*.png
📒 Files selected for processing (28)
  • app/src/main/java/com/flint/core/common/util/Constants.kt
  • app/src/main/java/com/flint/core/designsystem/component/button/FlintBasicButton.kt
  • app/src/main/java/com/flint/core/designsystem/component/button/FlintButtonState.kt
  • app/src/main/java/com/flint/core/designsystem/component/button/FlintMediumButton.kt
  • app/src/main/java/com/flint/core/navigation/Route.kt
  • app/src/main/java/com/flint/data/api/UserApi.kt
  • app/src/main/java/com/flint/data/dto/base/BaseEmptyResponse.kt
  • app/src/main/java/com/flint/data/dto/user/request/UpdateNicknameRequestDto.kt
  • app/src/main/java/com/flint/data/dto/user/request/UpdateProfileImageRequestDto.kt
  • app/src/main/java/com/flint/domain/repository/AuthRepository.kt
  • app/src/main/java/com/flint/domain/repository/UserRepository.kt
  • app/src/main/java/com/flint/presentation/main/MainNavHost.kt
  • app/src/main/java/com/flint/presentation/main/MainNavigator.kt
  • app/src/main/java/com/flint/presentation/profile/ProfileScreen.kt
  • app/src/main/java/com/flint/presentation/profile/navigation/ProfileNavigation.kt
  • app/src/main/java/com/flint/presentation/setting/SettingScreen.kt
  • app/src/main/java/com/flint/presentation/setting/SettingUiState.kt
  • app/src/main/java/com/flint/presentation/setting/SettingViewModel.kt
  • app/src/main/java/com/flint/presentation/setting/editprofile/EditProfileScreen.kt
  • app/src/main/java/com/flint/presentation/setting/editprofile/EditProfileUiState.kt
  • app/src/main/java/com/flint/presentation/setting/editprofile/EditProfileViewModel.kt
  • app/src/main/java/com/flint/presentation/setting/editprofile/navigation/EditProfileNavigation.kt
  • app/src/main/java/com/flint/presentation/setting/navigation/SettingNavigation.kt
  • app/src/main/java/com/flint/presentation/setting/withdraw/WithdrawCompleteScreen.kt
  • app/src/main/java/com/flint/presentation/setting/withdraw/WithdrawScreen.kt
  • app/src/main/java/com/flint/presentation/setting/withdraw/WithdrawViewModel.kt
  • app/src/main/java/com/flint/presentation/setting/withdraw/navigation/WithdrawCompleteNavigation.kt
  • app/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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

길이 검증이 UI 표시와 일관성이 부족합니다.

Line 25의 isLengthValidnickname.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.

Comment on lines +42 to +55
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) }
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Comment on lines +127 to +130
context.startActivity(
Intent(Intent.ACTION_VIEW, Uri.parse("https://artistic-bacon-a40.notion.site/35650cdb714e804cb642eb2cc576a62c"))
)
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

외부 링크 열기 시 예외 처리 추가 권장.

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).

Comment thread app/src/main/java/com/flint/presentation/setting/SettingUiState.kt Outdated
Comment thread app/src/main/java/com/flint/presentation/setting/SettingUiState.kt Outdated
Comment on lines +35 to +49
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) }
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Suggested change
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 ckals413 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[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>
@kimjw2003 kimjw2003 merged commit 47fb8ce into develop Jun 11, 2026
2 checks passed
@kimjw2003 kimjw2003 deleted the FLT-19-설정화면 branch June 11, 2026 16:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants