From 6f95fa8388e7733bfc139b782a5fb2b56cb302be Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Wed, 17 Jun 2026 14:45:51 +0900 Subject: [PATCH 1/3] =?UTF-8?q?refactor:=20refreshable=20fetch=20=ED=9D=90?= =?UTF-8?q?=EB=A6=84=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Home/List/TodoListView.swift | 2 +- .../PushNotificationListFeature.swift | 40 ++++++++++++------- .../PushNotificationListView.swift | 2 +- .../Sources/Today/TodayView.swift | 2 +- 4 files changed, 29 insertions(+), 17 deletions(-) diff --git a/Application/DevLogPresentation/Sources/Home/List/TodoListView.swift b/Application/DevLogPresentation/Sources/Home/List/TodoListView.swift index 9e5f9bf2..99fd0a28 100644 --- a/Application/DevLogPresentation/Sources/Home/List/TodoListView.swift +++ b/Application/DevLogPresentation/Sources/Home/List/TodoListView.swift @@ -171,7 +171,7 @@ struct TodoListView: View { } .offset(y: headerOffset) } - .refreshable { store.send(.view(.refresh)) } + .refreshable { await store.send(.view(.refresh)).finish() } .scrollDisabled(visibleTodos.isEmpty || store.state.isLoading) if store.state.isLoading { diff --git a/Application/DevLogPresentation/Sources/PushNotification/PushNotificationListFeature.swift b/Application/DevLogPresentation/Sources/PushNotification/PushNotificationListFeature.swift index 911f3340..cc38294d 100644 --- a/Application/DevLogPresentation/Sources/PushNotification/PushNotificationListFeature.swift +++ b/Application/DevLogPresentation/Sources/PushNotification/PushNotificationListFeature.swift @@ -58,6 +58,7 @@ struct PushNotificationListFeature { case loading(LoadingFeature.Action) enum ViewAction: Equatable { + case refresh case fetchNotifications case loadNextPage case deleteNotification(PushNotificationItem) @@ -83,7 +84,6 @@ struct PushNotificationListFeature { case syncNotifications([PushNotificationItem], nextCursor: PushNotificationCursor?, hasMore: Bool) case setNotificationHidden(String, Bool) case setNotificationRead(String, Bool) - case observeNotifications(PushNotificationQuery, Int) } } @@ -151,6 +151,9 @@ private extension PushNotificationListFeature { state: inout State ) -> Effect { switch action { + case .refresh: + state.nextCursor = nil + return fetchNotificationsPageEffect(query: state.query, cursor: nil) case .fetchNotifications: state.nextCursor = nil return fetchNotificationsEffect(query: state.query, cursor: nil, existingCount: 0) @@ -252,8 +255,6 @@ private extension PushNotificationListFeature { if let index = state.notifications.firstIndex(where: { $0.id == notificationId }) { state.notifications[index].isRead = isRead } - case .observeNotifications(let query, let limit): - return observeNotificationsEffect(query: query, limit: limit) } return .none @@ -272,7 +273,28 @@ private extension PushNotificationListFeature { existingCount: Int ) -> Effect { let limit = max(query.pageSize, existingCount) - let fetchEffect: Effect = .run { [fetchPushNotificationsUseCase] send in + let fetchEffect = fetchNotificationsPageEffect(query: query, cursor: cursor) + let observeEffect = observeNotificationsEffect( + query: query, + limit: max(limit, existingCount + query.pageSize) + ) + + if cursor == nil { + return .concatenate( + .cancel(id: CancelID.observeNotifications), + fetchEffect, + observeEffect + ) + } + + return fetchEffect + } + + func fetchNotificationsPageEffect( + query: PushNotificationQuery, + cursor: PushNotificationCursor? + ) -> Effect { + .run { [fetchPushNotificationsUseCase] send in await send(.loading(.begin(target: .default, mode: .delayed))) do { let page = try await fetchPushNotificationsUseCase.execute(query, cursor: cursor) @@ -286,7 +308,6 @@ private extension PushNotificationListFeature { )) ) await send(.store(.setHasMore(page.items.count == query.pageSize && page.nextCursor != nil))) - await send(.store(.observeNotifications(query, max(limit, existingCount + page.items.count)))) await send(.loading(.end(target: .default, mode: .delayed))) } catch { await send(.loading(.end(target: .default, mode: .delayed))) @@ -294,15 +315,6 @@ private extension PushNotificationListFeature { } } .cancellable(id: CancelID.fetchNotifications, cancelInFlight: true) - - if cursor == nil { - return .concatenate( - .cancel(id: CancelID.observeNotifications), - fetchEffect - ) - } - - return fetchEffect } func observeNotificationsEffect( diff --git a/Application/DevLogPresentation/Sources/PushNotification/PushNotificationListView.swift b/Application/DevLogPresentation/Sources/PushNotification/PushNotificationListView.swift index d6810f7c..d3c83677 100644 --- a/Application/DevLogPresentation/Sources/PushNotification/PushNotificationListView.swift +++ b/Application/DevLogPresentation/Sources/PushNotification/PushNotificationListView.swift @@ -38,7 +38,7 @@ struct PushNotificationListView: View { headerOffset = max(0, -offset) } .safeAreaInset(edge: .top) { safeAreaHeader } - .refreshable { store.send(.view(.fetchNotifications)) } + .refreshable { await store.send(.view(.refresh)).finish() } .navigationTitle(String(localized: "nav_push_notifications")) .listStyle(.plain) } diff --git a/Application/DevLogPresentation/Sources/Today/TodayView.swift b/Application/DevLogPresentation/Sources/Today/TodayView.swift index 834894d4..29eba6d7 100644 --- a/Application/DevLogPresentation/Sources/Today/TodayView.swift +++ b/Application/DevLogPresentation/Sources/Today/TodayView.swift @@ -39,7 +39,7 @@ struct TodayView: View { .navigationTitle(String(localized: "nav_today")) .toolbar { toolbarContent } .background(NavigationBarConfigurator()) - .refreshable { store.send(.refresh) } + .refreshable { await store.send(.refresh).finish() } .alert($store.scope(state: \.alert, action: \.alert)) .overlay { if store.isLoading { From c8d808b45148acbfd442a5422183123cab4ef389 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Wed, 17 Jun 2026 15:10:59 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20refreshable=20=EB=A1=9C=EB=94=A9=20?= =?UTF-8?q?=EC=9D=B8=EB=94=94=EC=BC=80=EC=9D=B4=ED=84=B0=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Home/List/TodoListFeature.swift | 17 +++++++++++----- .../Sources/Profile/ProfileFeature.swift | 20 ++++++++++++++----- .../PushNotificationListFeature.swift | 17 +++++++++++----- .../Sources/Today/TodayFeature.swift | 18 ++++++++++++----- 4 files changed, 52 insertions(+), 20 deletions(-) diff --git a/Application/DevLogPresentation/Sources/Home/List/TodoListFeature.swift b/Application/DevLogPresentation/Sources/Home/List/TodoListFeature.swift index d5af1df8..f1ff5846 100644 --- a/Application/DevLogPresentation/Sources/Home/List/TodoListFeature.swift +++ b/Application/DevLogPresentation/Sources/Home/List/TodoListFeature.swift @@ -201,10 +201,11 @@ private extension TodoListFeature { func fetchEffect( query: TodoQuery, cursor: TodoCursor?, - resetsPagination: Bool = true + resetsPagination: Bool = true, + showsIndicator: Bool = true ) -> Effect { .concatenate( - .send(.loading(.begin(target: .default, mode: .delayed))), + showsIndicator ? .send(.loading(.begin(target: .default, mode: .delayed))) : .none, .run { [fetchTodosUseCase] send in do { let page = try await fetchTodosUseCase.execute(query, cursor: cursor) @@ -216,12 +217,16 @@ private extension TodoListFeature { nextCursor: page.nextCursor ))) await send(.store(.setHasMore(page.items.count == query.pageSize && page.nextCursor != nil))) - await send(.loading(.end(target: .default, mode: .delayed))) + if showsIndicator { + await send(.loading(.end(target: .default, mode: .delayed))) + } } catch is CancellationError { return } catch { await send(.store(.setAlert(true))) - await send(.loading(.end(target: .default, mode: .delayed))) + if showsIndicator { + await send(.loading(.end(target: .default, mode: .delayed))) + } } } ) @@ -277,7 +282,9 @@ private extension TodoListFeature { state: inout State ) -> Effect { switch action { - case .refresh, .onAppear: + case .refresh: + return fetchEffect(query: state.query, cursor: nil, showsIndicator: false) + case .onAppear: return fetchEffect(query: state.query, cursor: nil) case .swipeTodo(let todo): return swipeTodoEffect(todo, state: &state) diff --git a/Application/DevLogPresentation/Sources/Profile/ProfileFeature.swift b/Application/DevLogPresentation/Sources/Profile/ProfileFeature.swift index 7250fafe..01164db2 100644 --- a/Application/DevLogPresentation/Sources/Profile/ProfileFeature.swift +++ b/Application/DevLogPresentation/Sources/Profile/ProfileFeature.swift @@ -108,10 +108,11 @@ struct ProfileFeature { if !settings.isEmpty { state.selectedActivityKinds = settings } + let showsIndicator = if case .fetchData = action { true } else { false } if let selectedQuarterStart = state.selectedQuarterStart { return .merge( fetchUserDataEffect(), - fetchActivityQuarterEffect(selectedQuarterStart) + fetchActivityQuarterEffect(selectedQuarterStart, showsIndicator: showsIndicator) ) } return fetchUserDataEffect() @@ -220,9 +221,14 @@ private extension ProfileFeature { } } - func fetchActivityQuarterEffect(_ quarterStart: Date) -> Effect { + func fetchActivityQuarterEffect( + _ quarterStart: Date, + showsIndicator: Bool = true + ) -> Effect { .run { [fetchTodosUseCase] send in - await send(.loading(.begin(target: .default, mode: .delayed))) + if showsIndicator { + await send(.loading(.begin(target: .default, mode: .delayed))) + } do { let data = try await ProfileHeatmapBuilder.fetchQuarterActivityData( from: quarterStart, @@ -237,9 +243,13 @@ private extension ProfileFeature { ) ) ) - await send(.loading(.end(target: .default, mode: .delayed))) + if showsIndicator { + await send(.loading(.end(target: .default, mode: .delayed))) + } } catch { - await send(.loading(.end(target: .default, mode: .delayed))) + if showsIndicator { + await send(.loading(.end(target: .default, mode: .delayed))) + } await send(.setAlert(true)) } } diff --git a/Application/DevLogPresentation/Sources/PushNotification/PushNotificationListFeature.swift b/Application/DevLogPresentation/Sources/PushNotification/PushNotificationListFeature.swift index cc38294d..3c270366 100644 --- a/Application/DevLogPresentation/Sources/PushNotification/PushNotificationListFeature.swift +++ b/Application/DevLogPresentation/Sources/PushNotification/PushNotificationListFeature.swift @@ -153,7 +153,7 @@ private extension PushNotificationListFeature { switch action { case .refresh: state.nextCursor = nil - return fetchNotificationsPageEffect(query: state.query, cursor: nil) + return fetchNotificationsPageEffect(query: state.query, cursor: nil, showsIndicator: false) case .fetchNotifications: state.nextCursor = nil return fetchNotificationsEffect(query: state.query, cursor: nil, existingCount: 0) @@ -292,10 +292,13 @@ private extension PushNotificationListFeature { func fetchNotificationsPageEffect( query: PushNotificationQuery, - cursor: PushNotificationCursor? + cursor: PushNotificationCursor?, + showsIndicator: Bool = true ) -> Effect { .run { [fetchPushNotificationsUseCase] send in - await send(.loading(.begin(target: .default, mode: .delayed))) + if showsIndicator { + await send(.loading(.begin(target: .default, mode: .delayed))) + } do { let page = try await fetchPushNotificationsUseCase.execute(query, cursor: cursor) if cursor == nil { @@ -308,9 +311,13 @@ private extension PushNotificationListFeature { )) ) await send(.store(.setHasMore(page.items.count == query.pageSize && page.nextCursor != nil))) - await send(.loading(.end(target: .default, mode: .delayed))) + if showsIndicator { + await send(.loading(.end(target: .default, mode: .delayed))) + } } catch { - await send(.loading(.end(target: .default, mode: .delayed))) + if showsIndicator { + await send(.loading(.end(target: .default, mode: .delayed))) + } await send(.store(.setAlert)) } } diff --git a/Application/DevLogPresentation/Sources/Today/TodayFeature.swift b/Application/DevLogPresentation/Sources/Today/TodayFeature.swift index 3c804c05..272b74b2 100644 --- a/Application/DevLogPresentation/Sources/Today/TodayFeature.swift +++ b/Application/DevLogPresentation/Sources/Today/TodayFeature.swift @@ -185,7 +185,9 @@ struct TodayFeature { return updateDisplayOptionsEffect(state.displayOptions) case .binding: break - case .refresh, .fetchData: + case .refresh: + return fetchTodosEffect(showsIndicator: false) + case .fetchData: return fetchTodosEffect() case .setSectionScope(let scope): if state.selectedSectionScope == scope, scope != .all { @@ -255,9 +257,11 @@ private enum UpdateTodayDisplayOptionsUseCaseKey: DependencyKey { } private extension TodayFeature { - func fetchTodosEffect() -> Effect { + func fetchTodosEffect(showsIndicator: Bool = true) -> Effect { .run { [fetchTodosUseCase] send in - await send(.loading(.begin(target: .default, mode: .delayed))) + if showsIndicator { + await send(.loading(.begin(target: .default, mode: .delayed))) + } do { async let todosWithDueDatePage = fetchTodosUseCase.execute( TodoQuery( @@ -284,9 +288,13 @@ private extension TodayFeature { let todosWithDueDate = try await todosWithDueDatePage.items.compactMap(TodayTodoItem.init(from:)) let todosWithoutDueDate = try await todosWithoutDueDatePage.items.compactMap(TodayTodoItem.init(from:)) await send(.store(.setTodos(todosWithDueDate + todosWithoutDueDate))) - await send(.loading(.end(target: .default, mode: .delayed))) + if showsIndicator { + await send(.loading(.end(target: .default, mode: .delayed))) + } } catch { - await send(.loading(.end(target: .default, mode: .delayed))) + if showsIndicator { + await send(.loading(.end(target: .default, mode: .delayed))) + } await send(.store(.setAlert)) } } From 959b85307a7575bdcbf46fac0c1a3cf0a51783fa Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Wed, 17 Jun 2026 22:45:03 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20refresh=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EA=B4=80=EC=B0=B0=20=EC=9E=AC=EA=B5=AC=EB=8F=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PushNotificationListFeature.swift | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Application/DevLogPresentation/Sources/PushNotification/PushNotificationListFeature.swift b/Application/DevLogPresentation/Sources/PushNotification/PushNotificationListFeature.swift index 3c270366..6cbde9d3 100644 --- a/Application/DevLogPresentation/Sources/PushNotification/PushNotificationListFeature.swift +++ b/Application/DevLogPresentation/Sources/PushNotification/PushNotificationListFeature.swift @@ -153,7 +153,12 @@ private extension PushNotificationListFeature { switch action { case .refresh: state.nextCursor = nil - return fetchNotificationsPageEffect(query: state.query, cursor: nil, showsIndicator: false) + return fetchNotificationsEffect( + query: state.query, + cursor: nil, + existingCount: 0, + showsIndicator: false + ) case .fetchNotifications: state.nextCursor = nil return fetchNotificationsEffect(query: state.query, cursor: nil, existingCount: 0) @@ -270,10 +275,11 @@ private extension PushNotificationListFeature { func fetchNotificationsEffect( query: PushNotificationQuery, cursor: PushNotificationCursor?, - existingCount: Int + existingCount: Int, + showsIndicator: Bool = true ) -> Effect { let limit = max(query.pageSize, existingCount) - let fetchEffect = fetchNotificationsPageEffect(query: query, cursor: cursor) + let fetchEffect = fetchNotificationsPageEffect(query: query, cursor: cursor, showsIndicator: showsIndicator) let observeEffect = observeNotificationsEffect( query: query, limit: max(limit, existingCount + query.pageSize)