Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 9 additions & 20 deletions Neki-iOS/APP/Sources/Application/AppCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -193,12 +193,8 @@ struct AppCoordinator {

switch newStatus {
case let .signedIn(user):
if case var .mainTab(mainTabState) = state.route {
mainTabState.user = user
state.route = .mainTab(.init(user: user))
return .none
}
state.route = .mainTab(.init(user: user))
if case .mainTab = state.route { return .none }
state.route = .mainTab(.init())
return .none

case .signedOut:
Expand All @@ -221,24 +217,17 @@ struct AppCoordinator {

case let .route(.auth(.delegate(.moveToMainTab(user)))):
state.$userSessionStatus.withLock { $0 = .signedIn(user) }
state.route = .mainTab(.init(user: user))
state.route = .mainTab(.init())
return .send(.executePendingShareExtensionIfNeeded)

case .route(.mainTab(.delegate(.signedOut))):
state.$userSessionStatus.withLock { $0 = .signedOut }
state.route = .auth(.init())
return .none

case .route(.mainTab(.delegate(.withdraw))):
case .route(.mainTab(.delegate(.signedOut))), .route(.mainTab(.delegate(.withdraw))):
state.$userSessionStatus.withLock { $0 = .signedOut }
state.initializeUserDefaults()
if case .route(.mainTab(.delegate(.withdraw))) = action {
state.initializeUserDefaults()
}
state.route = .auth(.init())
return .none

case let .route(.mainTab(.delegate(.profileUpdated(user)))):
state.$userSessionStatus.withLock { $0 = .signedIn(user) }
return .none

case .binding(\.isAlertPresented):
guard state.isAlertPresented == false else { return .none }
guard let pendingSessionStatus = state.pendingSessionStatus else { return .none }
Expand All @@ -253,8 +242,8 @@ struct AppCoordinator {

private func navigateToNextScreen(state: inout State, sessionStatus: UserSessionStatus) -> Effect<Action> {
switch sessionStatus {
case .signedIn(let user):
state.route = .mainTab(.init(user: user))
case .signedIn:
state.route = .mainTab(.init())
return .send(.executePendingShareExtensionIfNeeded)

case .signedOut, .expired:
Expand Down
20 changes: 11 additions & 9 deletions Neki-iOS/APP/Sources/MainTab/MainTabCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ struct MainTabCoordinator {

@ObservableState
struct State {
var user: User
@Shared(.appStorage(AppStorageKey.userSessionStatus)) var userSessionStatus: UserSessionStatus = .signedOut
var selectedTab: NekiTab = .archive

// 하위 코디네이터들의 State를 보유
var pose = PoseCoordinator.State()
var archive = ArchiveCoordinator.State()
var map = MapCoordinator.State()
var myPage: MyPageCoordinator.State
var myPage = MyPageCoordinator.State()

var imagePicker = ImagePickerFeature.State(mediaType: .photoBooth, autoUpload: false)

Expand All @@ -37,9 +37,15 @@ struct MainTabCoordinator {
var toast: NekiToastItem? = nil
var isPermissionAlertPresented: Bool = false

init(user: User) {
self.user = user
myPage = MyPageCoordinator.State(user: user)
var user: User {
get {
guard case let .signedIn(user) = userSessionStatus else { return .dummy }
return user
}

set {
$userSessionStatus.withLock { $0 = .signedIn(newValue) }
}
}
}

Expand Down Expand Up @@ -72,7 +78,6 @@ struct MainTabCoordinator {
enum Delegate {
case signedOut
case withdraw
case profileUpdated(User)
}
}

Expand Down Expand Up @@ -170,9 +175,6 @@ struct MainTabCoordinator {
await send(.delegate(.withdraw))
}

case let .myPage(.delegate(.profileUpdated(user))):
return .send(.delegate(.profileUpdated(user)))

case let .imagePicker(.delegate(.imagesConverted(entities))):
state.isPhotoPickerPresented = false
state.isLoading = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@ public struct User: Sendable, Equatable, Codable {
let providerType: ProviderType
var allRequiredTermsAgreed: Bool
}

extension User {
static var dummy: Self { User(id: -1, nickname: "-", email: nil, profileImageURL: nil, providerType: .local, allRequiredTermsAgreed: true) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,10 @@ import ComposableArchitecture
struct MyPageCoordinator {
@ObservableState
struct State {
var root: MyPageFeature.State
var path = StackState<Path.State>()
@Shared(.appStorage(AppStorageKey.userSessionStatus)) var userSessionStatus: UserSessionStatus = .signedOut

init(user: User) {
root = MyPageFeature.State(user: user)
}
var root = MyPageFeature.State()
var path = StackState<Path.State>()
}

enum Action {
Expand All @@ -28,7 +26,6 @@ struct MyPageCoordinator {
enum Delegate {
case didLogout
case didWithdraw
case profileUpdated(User)
}
}

Expand All @@ -43,11 +40,12 @@ struct MyPageCoordinator {
return routeMyPageCellTapped(state: &state, cellItem)

case .root(.profileTapped):
state.path.append(.accountPreference(.init(user: state.root.user)))
state.path.append(.accountPreference(.init()))
return .none

case .path(.element(id: _, action: .accountPreference(.editProfileButtonTapped))):
state.path.append(.profileEdit(.init(user: state.root.user)))
guard case let .signedIn(user) = state.userSessionStatus else { return .none }
state.path.append(.profileEdit(.init(user: user)))
return .none

case .path(.element(id: _, action: .accountPreference(.didSignOut))):
Expand All @@ -56,9 +54,6 @@ struct MyPageCoordinator {
case .path(.element(id: _, action: .accountPreference(.didWithdraw))):
return .send(.delegate(.didWithdraw))

case let .path(.element(id: _, action: .profileEdit(.profileUpdated(user)))):
return .send(.delegate(.profileUpdated(user)))

default:
return .none
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,16 @@ import os
struct AccountPreferenceFeature {
@ObservableState
struct State {
var user: User
@Shared(.appStorage(AppStorageKey.userSessionStatus)) var userSessionStatus: UserSessionStatus = .signedOut

var isLogoutAlertPresented: Bool = false
var isUnregisterAlertPresented: Bool = false
var isLoading: Bool = false

var user: User {
guard case let .signedIn(user) = userSessionStatus else { return .dummy }
return user
}
}

enum Action: BindableAction {
Expand Down Expand Up @@ -71,9 +76,9 @@ struct AccountPreferenceFeature {
state.isUnregisterAlertPresented = false
state.isLoading = true

return .run { [userId = state.user.id] send in
return .run { [userID = state.user.id] send in
try await authClient.withdraw()
UserDefaults.standard.removeObject(forKey: "TermsAgreed_\(userId)")
UserDefaults.standard.removeObject(forKey: "TermsAgreed_\(userID)")
await send(.didWithdraw)
} catch: { error, send in
Logger.presentation.error("회원탈퇴 과정 중 에러 발생: \(error)")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,13 @@ import ComposableArchitecture
struct MyPageFeature {
@ObservableState
struct State {
var user: User
@Shared(.appStorage(AppStorageKey.userSessionStatus)) var userSessionStatus: UserSessionStatus = .signedOut
var appVersion: AppVersion = AppVersion(major: 0, minor: 0, revision: 0)

var user: User {
guard case let .signedIn(user) = userSessionStatus else { return .dummy }
return user
}
}

enum Action {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import os
struct ProfileEditFeature {
@ObservableState
struct State {
@ObservationStateIgnored let user: User
@Shared(.appStorage(AppStorageKey.userSessionStatus)) var userSessionStatus: UserSessionStatus = .signedOut
var nickname: String
var currentProfileImageURL: URL?
var selectedProfileImage: UIImage?
Expand All @@ -29,8 +29,18 @@ struct ProfileEditFeature {

var isProfileSelectionAlertPresented: Bool = false

var user: User {
get {
guard case let .signedIn(user) = userSessionStatus else { return .dummy }
return user
}

set {
$userSessionStatus.withLock { $0 = .signedIn(newValue) }
}
}

init(user: User) {
self.user = user
nickname = user.nickname
currentProfileImageURL = user.profileImageURL
}
Expand All @@ -50,9 +60,6 @@ struct ProfileEditFeature {

// Binding Actions
case binding(BindingAction<State>)

// Delegate Actions
case profileUpdated(User)
}

private enum CancelID { case imageLoad }
Expand Down Expand Up @@ -134,8 +141,8 @@ struct ProfileEditFeature {

case let .updateProfileResponse(.success(user)):
state.isLoading = false
return .run { send in
await send(.profileUpdated(user))
state.user = user
return .run { _ in
await dismiss()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,20 @@ private extension AccountPreferenceView {

var profileArea: some View {
VStack(spacing: 16) {
KFImage(store.user.profileImageURL)
.resizable()
.onFailureImage(.iconDefaultProfile)
.scaledToFill()
.frame(width: 142, height: 142)
.clipShape(.circle)
ZStack(alignment: .bottomTrailing) {
KFImage(store.user.profileImageURL)
.resizable()
.onFailureImage(.iconDefaultProfile)
.scaledToFill()
.frame(width: 142, height: 142)
.clipShape(.circle)

Button {
store.send(.editProfileButtonTapped)
} label: {
Comment on lines +80 to +82
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Apr 8, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 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 swift

Repository: 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

의도한 사항이므로 기각.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@Remaked-Swain, 이해했습니다. PR 설명에서도 해당 이중 탭 흐름(계정 설정 화면 → 프로필 편집 화면 → 프로필 변경)이 의도적으로 허용된 동작임을 명시하셨군요. 감사합니다!

(╯°□°)╯

Image(.iconProfileCamera)
}
Comment on lines +80 to +84
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Apr 8, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 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 swift

Repository: 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

당장은 접근성 관련 리팩토링을 진행할 예정이 없으므로 기각.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@Remaked-Swain, 알겠습니다. 나중에 접근성 개선을 진행하실 때 참고할 수 있도록 이슈로 남겨드릴까요?

(´・ω・`)

}

HStack(spacing: 9) {
Text(store.user.nickname)
Expand Down