Conversation
Walkthrough프로필 이미지에 편집 버튼을 오버레이하고, 사용자 세션 상태를 AppStorage 기반의 공유 상태( Changes
Sequence Diagram(s)sequenceDiagram
participant AppCoord as AppCoordinator
participant Storage as AppStorage\n(UserSessionStatus)
participant MainTab as MainTabCoordinator
participant MyPage as MyPageCoordinator
participant ProfileEdit as ProfileEditFeature
AppCoord->>Storage: userSessionStatus = .signedIn(user)
AppCoord->>MainTab: route = .mainTab() (init)
MainTab->>MyPage: init root (reads user from Storage)
MyPage->>MyPage: user = Storage.signedIn? -> computed user
MyPage->>AccountPref: profileTapped -> push accountPreference
AccountPref->>ProfileEdit: editProfileButtonTapped -> push profileEdit
ProfileEdit->>Storage: update userSessionStatus = .signedIn(updatedUser)
Storage-->>MyPage: (shared) computed user now reflects updatedUser
MyPage-->>UI: UI reflects updatedUser
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@Neki-iOS/Features/MyPage/Sources/Presentation/Sources/View/AccountPreferenceView.swift`:
- Around line 80-82: The camera/image Button currently sends
store.send(.editProfileButtonTapped) which routes via MyPageCoordinator to
.profileEdit(...) and thus opens the full profile edit screen (same as the
nickname pencil), so change the flow by adding a distinct action (e.g.
editProfileImageButtonTapped) to the action enum and reducer, update the Button
to send that new action, and add a new coordinator route/handling for the
image-only flow (e.g. profileImageEdit or a direct image picker entry) in
MyPageCoordinator where you previously route .profileEdit(.init(user:
state.root.user)); also add an accessibility label to the image button (e.g.
.accessibilityLabel("프로필 사진 변경")) to improve VoiceOver support.
- Around line 80-84: The camera Image button (Button sending
store.send(.editProfileButtonTapped) with Image(.iconProfileCamera)) lacks
accessible labeling and a minimum tappable area; update the Button to provide an
accessibilityLabel (e.g., "Edit profile photo"), an accessibilityHint (e.g.,
"Opens camera to change your profile picture"), ensure it has
.accessibilityAddTraits(.isButton), and enlarge its tap target by applying a
minimum frame (frame(minWidth: 44, minHeight: 44)) or additional padding plus
contentShape(Rectangle()) so the hit area meets 44pt minimum while keeping the
existing action and image.
🪄 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
Run ID: 83a2af7c-4027-4be6-965e-3f186e4606b2
📒 Files selected for processing (1)
Neki-iOS/Features/MyPage/Sources/Presentation/Sources/View/AccountPreferenceView.swift
| Button { | ||
| store.send(.editProfileButtonTapped) | ||
| } label: { |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify whether editProfileButtonTapped routes directly to image picking
# or just navigates to profile edit screen.
fd -i 'AccountPreferenceFeature.swift' --exec sed -n '1,260p' {}
rg -n -C4 '\beditProfileButtonTapped\b' --type swift
rg -n -C4 '\b(profile|camera|image|picker|photo)\b' --type swiftRepository: YAPP-Github/Neki-iOS
Length of output: 50376
카메라 버튼이 프로필 편집 화면을 거쳐 기대되는 직접 사진 변경 플로우를 구현하지 못합니다.
Line 81의 .editProfileButtonTapped 액션은 MyPageCoordinator.swift:49–50에서 .profileEdit(.init(user: state.root.user))로 라우팅되어, 결과적으로 프로필 편집 화면으로 네비게이션됩니다. 이는 Line 93의 닉네임 편집(연필 아이콘)과 동일한 플로우입니다. 따라서 카메라 버튼은 "바로 사진 변경" 기능이 아니라 기존 편집 화면을 거치게 됩니다. 카메라 선택을 위해 별도의 액션(editProfileImageButtonTapped 등) 및 라우팅 경로를 추가하여 구현하시기 바랍니다.
추가로 Line 83의 이미지 버튼은 명시적 접근성 레이블 없이 아이콘만 표시되어, VoiceOver 사용자 경험이 저하될 수 있습니다. .accessibilityLabel("프로필 사진 변경") 등의 수정자를 추가하세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@Neki-iOS/Features/MyPage/Sources/Presentation/Sources/View/AccountPreferenceView.swift`
around lines 80 - 82, The camera/image Button currently sends
store.send(.editProfileButtonTapped) which routes via MyPageCoordinator to
.profileEdit(...) and thus opens the full profile edit screen (same as the
nickname pencil), so change the flow by adding a distinct action (e.g.
editProfileImageButtonTapped) to the action enum and reducer, update the Button
to send that new action, and add a new coordinator route/handling for the
image-only flow (e.g. profileImageEdit or a direct image picker entry) in
MyPageCoordinator where you previously route .profileEdit(.init(user:
state.root.user)); also add an accessibility label to the image button (e.g.
.accessibilityLabel("프로필 사진 변경")) to improve VoiceOver support.
There was a problem hiding this comment.
@Remaked-Swain, 이해했습니다. PR 설명에서도 해당 이중 탭 흐름(계정 설정 화면 → 프로필 편집 화면 → 프로필 변경)이 의도적으로 허용된 동작임을 명시하셨군요. 감사합니다!
(╯°□°)╯
| Button { | ||
| store.send(.editProfileButtonTapped) | ||
| } label: { | ||
| Image(.iconProfileCamera) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify accessibility metadata presence for the new camera button area.
rg -n -C3 'iconProfileCamera|accessibilityLabel|accessibilityHint|minWidth:\s*44|minHeight:\s*44' --type swiftRepository: YAPP-Github/Neki-iOS
Length of output: 1728
🏁 Script executed:
# Read the full context around the button in AccountPreferenceView.swift
cat -n Neki-iOS/Features/MyPage/Sources/Presentation/Sources/View/AccountPreferenceView.swift | sed -n '70,95p'Repository: YAPP-Github/Neki-iOS
Length of output: 1163
카메라 버튼의 접근성(VoiceOver 라벨/탭 영역) 개선이 필요합니다.
이미지 단독 버튼은 의미 전달이 약하고 탭 타깃이 작을 수 있습니다. 최소 탭 영역(44pt)과 접근성 라벨/힌트를 추가해주세요.
개선 예시
Button {
store.send(.editProfileButtonTapped)
} label: {
Image(.iconProfileCamera)
+ .padding(10)
+ .background(.white)
+ .clipShape(Circle())
}
+ .frame(minWidth: 44, minHeight: 44)
+ .contentShape(Rectangle())
+ .accessibilityLabel("프로필 사진 변경")
+ .accessibilityHint("프로필 사진을 변경하는 화면으로 이동")🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@Neki-iOS/Features/MyPage/Sources/Presentation/Sources/View/AccountPreferenceView.swift`
around lines 80 - 84, The camera Image button (Button sending
store.send(.editProfileButtonTapped) with Image(.iconProfileCamera)) lacks
accessible labeling and a minimum tappable area; update the Button to provide an
accessibilityLabel (e.g., "Edit profile photo"), an accessibilityHint (e.g.,
"Opens camera to change your profile picture"), ensure it has
.accessibilityAddTraits(.isButton), and enlarge its tap target by applying a
minimum frame (frame(minWidth: 44, minHeight: 44)) or additional padding plus
contentShape(Rectangle()) so the hit area meets 44pt minimum while keeping the
existing action and image.
There was a problem hiding this comment.
당장은 접근성 관련 리팩토링을 진행할 예정이 없으므로 기각.
There was a problem hiding this comment.
@Remaked-Swain, 알겠습니다. 나중에 접근성 개선을 진행하실 때 참고할 수 있도록 이슈로 남겨드릴까요?
(´・ω・`)
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
Neki-iOS/APP/Sources/Application/AppCoordinator.swift (1)
200-211:⚠️ Potential issue | 🟡 Minor잠재적 로직 오류:
.signedOut케이스에서 불필요한 guard 문Line 202의
guard case .expired = newStatus else { return .none }은.signedOut케이스 내부에 있으므로newStatus가.signedOut일 때 항상 early return 됩니다. 이로 인해 토스트 메시지가 절대 표시되지 않습니다.의도된 동작이라면 불필요한 코드이고, 버그라면 토스트가 표시되지 않는 문제가 있습니다.
🔧 의도에 따른 수정 제안
토스트가
.signedOut에서 필요 없는 경우:case .signedOut: state.route = .auth(.init()) - guard case .expired = newStatus else { return .none } - state.toastItem = .init("다시 로그인 해주세요.") return .none case .expired: state.route = .auth(.init()) - guard case .expired = newStatus else { return .none } state.toastItem = .init("다시 로그인 해주세요.") return .none🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Neki-iOS/APP/Sources/Application/AppCoordinator.swift` around lines 200 - 211, The .signedOut case contains an erroneous guard "guard case .expired = newStatus else { return .none }" which will always fail when handling .signedOut and prevents the toast from ever being set; remove that guard from the .signedOut branch so state.route = .auth(.init()) is followed by state.toastItem = .init("다시 로그인 해주세요.") and return .none, or alternatively, if the toast should only appear when newStatus == .expired move the expiration check out of the .signedOut branch and perform "guard case .expired = newStatus" before setting the toast; update the code referencing state.route, newStatus, and state.toastItem accordingly.
🧹 Nitpick comments (2)
Neki-iOS/Features/MyPage/Sources/Presentation/Sources/Feature/AccountPreferenceFeature.swift (1)
75-86:userID캡처 시 잠재적 엣지 케이스Line 79에서
state.user.id를 캡처할 때, 이론적으로userSessionStatus가.signedIn이 아닌 경우User.dummy의id: -1이 캡처될 수 있습니다. 이 경우"TermsAgreed_-1"키가 삭제되어 실제 사용자의 동의 정보가 남게 됩니다.현재 흐름에서는 로그인된 상태에서만 회원탈퇴 버튼이 노출되므로 실제 문제가 발생할 가능성은 낮지만, 방어적 코딩을 권장합니다.
🛡️ 방어적 코딩 제안
case .unregisterButtonTapped: state.isUnregisterAlertPresented = false + guard case let .signedIn(user) = state.userSessionStatus else { return .none } state.isLoading = true - return .run { [userID = state.user.id] send in + return .run { [userID = user.id] send in try await authClient.withdraw() UserDefaults.standard.removeObject(forKey: "TermsAgreed_\(userID)") await send(.didWithdraw)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Neki-iOS/Features/MyPage/Sources/Presentation/Sources/Feature/AccountPreferenceFeature.swift` around lines 75 - 86, The current .unregisterButtonTapped run task captures state.user.id which may be User.dummy (-1) in edge cases; change the capture and deletion to be defensive: capture a validated userID (from state.user.id or from authClient/session) before calling authClient.withdraw, and only call UserDefaults.standard.removeObject(forKey: "TermsAgreed_\(userID)") if that userID is a valid signed-in id (e.g., not -1 or nil); update the .unregisterButtonTapped handler to validate the userID before removing the key and to avoid deleting "TermsAgreed_-1".Neki-iOS/APP/Sources/Application/AppCoordinator.swift (1)
194-198: 사용되지 않는user변수 - 컴파일러 경고 발생 가능
case let .signedIn(user)에서 추출된user변수가 사용되지 않아 컴파일러 경고가 발생할 수 있습니다.🔧 제안된 수정
- case let .signedIn(user): + case .signedIn: if case .mainTab = state.route { return .none } state.route = .mainTab(.init()) return .none🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Neki-iOS/APP/Sources/Application/AppCoordinator.swift` around lines 194 - 198, The pattern match binds an unused user in the switch (case let .signedIn(user)) causing a compiler warning; change the pattern to not bind the value (use case .signedIn or case let .signedIn(_) ) in the switch on newStatus to eliminate the unused-variable warning while keeping the existing logic that sets state.route = .mainTab(.init()) and early-returns when state.route is already .mainTab.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@Neki-iOS/APP/Sources/Application/AppCoordinator.swift`:
- Around line 200-211: The .signedOut case contains an erroneous guard "guard
case .expired = newStatus else { return .none }" which will always fail when
handling .signedOut and prevents the toast from ever being set; remove that
guard from the .signedOut branch so state.route = .auth(.init()) is followed by
state.toastItem = .init("다시 로그인 해주세요.") and return .none, or alternatively, if
the toast should only appear when newStatus == .expired move the expiration
check out of the .signedOut branch and perform "guard case .expired = newStatus"
before setting the toast; update the code referencing state.route, newStatus,
and state.toastItem accordingly.
---
Nitpick comments:
In `@Neki-iOS/APP/Sources/Application/AppCoordinator.swift`:
- Around line 194-198: The pattern match binds an unused user in the switch
(case let .signedIn(user)) causing a compiler warning; change the pattern to not
bind the value (use case .signedIn or case let .signedIn(_) ) in the switch on
newStatus to eliminate the unused-variable warning while keeping the existing
logic that sets state.route = .mainTab(.init()) and early-returns when
state.route is already .mainTab.
In
`@Neki-iOS/Features/MyPage/Sources/Presentation/Sources/Feature/AccountPreferenceFeature.swift`:
- Around line 75-86: The current .unregisterButtonTapped run task captures
state.user.id which may be User.dummy (-1) in edge cases; change the capture and
deletion to be defensive: capture a validated userID (from state.user.id or from
authClient/session) before calling authClient.withdraw, and only call
UserDefaults.standard.removeObject(forKey: "TermsAgreed_\(userID)") if that
userID is a valid signed-in id (e.g., not -1 or nil); update the
.unregisterButtonTapped handler to validate the userID before removing the key
and to avoid deleting "TermsAgreed_-1".
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: d1d1c30d-145a-46d4-a19c-aa21e63c1f32
📒 Files selected for processing (7)
Neki-iOS/APP/Sources/Application/AppCoordinator.swiftNeki-iOS/APP/Sources/MainTab/MainTabCoordinator.swiftNeki-iOS/Core/Sources/Auth/Sources/Domain/Sources/Entities/User.swiftNeki-iOS/Features/MyPage/Sources/Presentation/Sources/Coordinator/MyPageCoordinator.swiftNeki-iOS/Features/MyPage/Sources/Presentation/Sources/Feature/AccountPreferenceFeature.swiftNeki-iOS/Features/MyPage/Sources/Presentation/Sources/Feature/MyPageFeature.swiftNeki-iOS/Features/MyPage/Sources/Presentation/Sources/Feature/ProfileEditFeature.swift
🌴 작업한 브랜치
✅ 작업한 내용
계정 설정 화면에서 보이는 프로필 사진 영역에 카메라 모양의 버튼을 제공합니다.
해당 버튼을 누르면 프로필 사진 변경이 가능하다는 시각적 UX를 제공하여 프로필 사진 변경 기능의 사용성을 용이하게 했습니다.
또 프로필 편집 후에 AppCoordinator 수준에서 새로운 유저 정보로 아예 덮어씌워버리니까 프로필 편집 화면의 이전 화면이 아닌, 아카이빙 탭이 나타나는 문제를 발견하여 조치했습니다.
이제 메인탭 상태를 초기화하지 않고 유지하는 것으로 기존에 열려있던 탭인 마이페이지 탭이 그대로 살아있게 합니다.
❗️PR Point
해당 이슈는 계정 설정 화면에서 프로필 사진 변경을 어떻게 하느냐라는 기능 사용성의 불편함에서 비롯되었습니다.
그러나 프로필 사진 변경을 위해 프로필 사진 영역을 두 번 탭해야 하는 새로운 불편함을 초래하기도 했습니다.
(계정 설정 화면에서 프사 터치 -> 프로필 편집 화면으로 이동 -> 프로필 편집 화면에서 프사 터치 -> 프로필 변경 기능)
기디 수준에서 재검토가 필요할 수 있습니다.
(사실 PM에게는 이미 알려줬음)
번외로, 프로필 정보 변경 관련해 메인탭 상태 변경 로직을 디버깅하는 과정에서 다음의 수정이 있었습니다.
@Shared상태 공유변경의 이유는 다음과 같습니다.
아래의 사항을 주로 고민했습니다.
UserSessionStatus는 열거형이므로, 로그인된 상태에서만 접근할 수 있는 화면임에도 불구하고 비로그인 상태까지 고려하여 옵셔널로 선언해야함.store.user?.nickname처럼 코드를 작성하지 않도록 만듦.📸 스크린샷
작은 변경사항이라 생략합니다.
📟 관련 이슈
Summary by CodeRabbit
릴리스 노트