Skip to content

Commit fc1b992

Browse files
committed
fix: 푸시 알람을 탭해서 앱에 들어와도 푸시알림 데이터가 최신화되지 않는 현상 해결
1 parent 5f3878e commit fc1b992

7 files changed

Lines changed: 165 additions & 30 deletions

File tree

DevLog/Data/Repository/PushNotificationRepositoryImpl.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77

88
import Foundation
9+
import Combine
910

1011
final class PushNotificationRepositoryImpl: PushNotificationRepository {
1112
private let service: PushNotificationService
@@ -41,6 +42,15 @@ final class PushNotificationRepositoryImpl: PushNotificationRepository {
4142
return try response.toDomain()
4243
}
4344

45+
func observeNotifications(
46+
_ query: PushNotificationQuery,
47+
limit: Int
48+
) throws -> AnyPublisher<PushNotificationPage, Error> {
49+
try service.observeNotifications(query, limit: limit)
50+
.tryMap { try $0.toDomain() }
51+
.eraseToAnyPublisher()
52+
}
53+
4454
// 푸시 알림 기록 삭제
4555
func deleteNotification(_ notificationID: String) async throws {
4656
try await service.deleteNotification(notificationID)

DevLog/Domain/Protocol/PushNotificationRepository.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77

88
import Foundation
9+
import Combine
910

1011
protocol PushNotificationRepository {
1112
func fetchPushNotificationEnabled() async throws -> Bool
@@ -15,6 +16,10 @@ protocol PushNotificationRepository {
1516
_ query: PushNotificationQuery,
1617
cursor: PushNotificationCursor?
1718
) async throws -> PushNotificationPage
19+
func observeNotifications(
20+
_ query: PushNotificationQuery,
21+
limit: Int
22+
) throws -> AnyPublisher<PushNotificationPage, Error>
1823
func deleteNotification(_ notificationID: String) async throws
1924
func toggleNotificationRead(_ todoId: String) async throws
2025
}

DevLog/Domain/UseCase/PushNotification/Fetch/FetchPushNotificationsUseCase.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,16 @@
55
// Created by 최윤진 on 2/10/26.
66
//
77

8+
import Combine
9+
810
protocol FetchPushNotificationsUseCase {
911
func execute(
1012
_ query: PushNotificationQuery,
1113
cursor: PushNotificationCursor?
1214
) async throws -> PushNotificationPage
15+
16+
func observe(
17+
_ query: PushNotificationQuery,
18+
limit: Int
19+
) throws -> AnyPublisher<PushNotificationPage, Error>
1320
}

DevLog/Domain/UseCase/PushNotification/Fetch/FetchPushNotificationsUseCaseImpl.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
// Created by 최윤진 on 2/10/26.
66
//
77

8+
import Combine
9+
810
final class FetchPushNotificationsUseCaseImpl: FetchPushNotificationsUseCase {
911
private let repository: PushNotificationRepository
1012

@@ -18,4 +20,13 @@ final class FetchPushNotificationsUseCaseImpl: FetchPushNotificationsUseCase {
1820
) async throws -> PushNotificationPage {
1921
try await repository.requestNotifications(query, cursor: cursor)
2022
}
23+
24+
func observe(
25+
_ query: PushNotificationQuery,
26+
limit: Int
27+
) throws -> AnyPublisher<PushNotificationPage, Error> {
28+
try repository.observeNotifications(query, limit: limit)
29+
.removeDuplicates()
30+
.eraseToAnyPublisher()
31+
}
2132
}

DevLog/Infra/Service/PushNotificationService.swift

Lines changed: 81 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77

88
import FirebaseAuth
9+
import Combine
910
import FirebaseFirestore
1011

1112
final class PushNotificationService {
@@ -90,28 +91,12 @@ final class PushNotificationService {
9091

9192
/// 푸시 알림 기록 요청
9293
func requestNotifications(
93-
_ query: PushNotificationQuery,
94+
_ notificationQuery: PushNotificationQuery,
9495
cursor: PushNotificationCursorDTO?
9596
) async throws -> PushNotificationPageResponse {
9697
guard let uid = Auth.auth().currentUser?.uid else { throw AuthError.notAuthenticated }
9798

98-
var firestoreQuery: Query = store.collection("users/\(uid)/notifications")
99-
100-
if let thresholdDate = query.timeFilter.thresholdDate {
101-
firestoreQuery = firestoreQuery.whereField(
102-
"receivedAt",
103-
isGreaterThanOrEqualTo: Timestamp(date: thresholdDate)
104-
)
105-
}
106-
107-
if query.unreadOnly {
108-
firestoreQuery = firestoreQuery.whereField("isRead", isEqualTo: false)
109-
}
110-
111-
let isDescending = query.sortOrder == .latest
112-
firestoreQuery = firestoreQuery
113-
.order(by: "receivedAt", descending: isDescending)
114-
.order(by: FieldPath.documentID())
99+
var firestoreQuery = makeQuery(uid: uid, query: notificationQuery)
115100

116101
if let cursor {
117102
firestoreQuery = firestoreQuery.start(after: [
@@ -121,13 +106,13 @@ final class PushNotificationService {
121106
}
122107

123108
let snapshot = try await firestoreQuery
124-
.limit(to: query.pageSize)
109+
.limit(to: notificationQuery.pageSize)
125110
.getDocuments()
126111

127112
let items = snapshot.documents.compactMap { makeResponse(from: $0) }
128113

129114
let nextCursor: PushNotificationCursorDTO? = snapshot.documents.last.map { document in
130-
guard let receivedAt = document.data()[NotificationFieldKey.receivedAt.rawValue] as? Timestamp else {
115+
guard let receivedAt = document.data()[Key.receivedAt.rawValue] as? Timestamp else {
131116
return nil
132117
}
133118

@@ -140,6 +125,39 @@ final class PushNotificationService {
140125
return PushNotificationPageResponse(items: items, nextCursor: nextCursor)
141126
}
142127

128+
func observeNotifications(
129+
_ query: PushNotificationQuery,
130+
limit: Int
131+
) throws -> AnyPublisher<PushNotificationPageResponse, Error> {
132+
guard let uid = Auth.auth().currentUser?.uid else { throw AuthError.notAuthenticated }
133+
134+
let subject = PassthroughSubject<PushNotificationPageResponse, Error>()
135+
let pageLimit = max(query.pageSize, limit)
136+
let listener = makeQuery(uid: uid, query: query)
137+
.limit(to: pageLimit)
138+
.addSnapshotListener { [weak self] snapshot, error in
139+
if let error {
140+
subject.send(completion: .failure(error))
141+
return
142+
}
143+
144+
guard let self, let snapshot else { return }
145+
146+
let items = snapshot.documents.compactMap { self.makeResponse(from: $0) }
147+
let nextCursor = self.makeNextCursor(from: snapshot.documents.last)
148+
subject.send(
149+
PushNotificationPageResponse(
150+
items: items,
151+
nextCursor: nextCursor
152+
)
153+
)
154+
}
155+
156+
return subject
157+
.handleEvents(receiveCancel: { listener.remove() })
158+
.eraseToAnyPublisher()
159+
}
160+
143161
/// 푸시 알림 기록 삭제
144162
func deleteNotification(_ notificationID: String) async throws {
145163
guard let uid = Auth.auth().currentUser?.uid else { throw AuthError.notAuthenticated }
@@ -177,15 +195,51 @@ final class PushNotificationService {
177195
}
178196

179197
private extension PushNotificationService {
198+
func makeQuery(
199+
uid: String,
200+
query: PushNotificationQuery
201+
) -> Query {
202+
var firestoreQuery: Query = store.collection("users/\(uid)/notifications")
203+
204+
if let thresholdDate = query.timeFilter.thresholdDate {
205+
firestoreQuery = firestoreQuery.whereField(
206+
"receivedAt",
207+
isGreaterThanOrEqualTo: Timestamp(date: thresholdDate)
208+
)
209+
}
210+
211+
if query.unreadOnly {
212+
firestoreQuery = firestoreQuery.whereField("isRead", isEqualTo: false)
213+
}
214+
215+
let isDescending = query.sortOrder == .latest
216+
return firestoreQuery
217+
.order(by: "receivedAt", descending: isDescending)
218+
.order(by: FieldPath.documentID())
219+
}
220+
221+
func makeNextCursor(from document: QueryDocumentSnapshot?) -> PushNotificationCursorDTO? {
222+
guard
223+
let document,
224+
let receivedAt = document.data()[Key.receivedAt.rawValue] as? Timestamp else {
225+
return nil
226+
}
227+
228+
return PushNotificationCursorDTO(
229+
receivedAt: receivedAt.dateValue(),
230+
documentID: document.documentID
231+
)
232+
}
233+
180234
func makeResponse(from snapshot: QueryDocumentSnapshot) -> PushNotificationResponse? {
181235
let data = snapshot.data()
182236
guard
183-
let title = data[NotificationFieldKey.title.rawValue] as? String,
184-
let body = data[NotificationFieldKey.body.rawValue] as? String,
185-
let receivedAt = data[NotificationFieldKey.receivedAt.rawValue] as? Timestamp,
186-
let isRead = data[NotificationFieldKey.isRead.rawValue] as? Bool,
187-
let todoId = data[NotificationFieldKey.todoId.rawValue] as? String,
188-
let todoKind = data[NotificationFieldKey.todoKind.rawValue] as? String else {
237+
let title = data[Key.title.rawValue] as? String,
238+
let body = data[Key.body.rawValue] as? String,
239+
let receivedAt = data[Key.receivedAt.rawValue] as? Timestamp,
240+
let isRead = data[Key.isRead.rawValue] as? Bool,
241+
let todoId = data[Key.todoId.rawValue] as? String,
242+
let todoKind = data[Key.todoKind.rawValue] as? String else {
189243
return nil
190244
}
191245

@@ -200,7 +254,7 @@ private extension PushNotificationService {
200254
)
201255
}
202256

203-
enum NotificationFieldKey: String {
257+
enum Key: String {
204258
case title
205259
case body
206260
case receivedAt

DevLog/Infra/Service/TodoService.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ final class TodoService {
3737
]
3838
logger.info("Fetching todo page: \(logComponents.compactMap { $0 }.joined(separator: ", "))")
3939

40-
var firestoreQuery: Query = makeOrderedQuery(uid: uid, query: query)
40+
var firestoreQuery = makeQuery(uid: uid, query: query)
4141

4242
if let kind = query.kind {
4343
firestoreQuery = firestoreQuery.whereField("kind", isEqualTo: kind.rawValue)
@@ -209,7 +209,7 @@ final class TodoService {
209209
}
210210

211211
private extension TodoService {
212-
func makeOrderedQuery(uid: String, query: TodoQuery) -> Query {
212+
func makeQuery(uid: String, query: TodoQuery) -> Query {
213213
let collection = store.collection("users/\(uid)/todoLists/")
214214

215215
switch query.sortTarget {

DevLog/Presentation/ViewModel/PushNotificationListViewModel.swift

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77

88
import Foundation
9+
import Combine
910

1011
@Observable
1112
final class PushNotificationListViewModel: Store {
@@ -36,6 +37,7 @@ final class PushNotificationListViewModel: Store {
3637
case appendNotifications([PushNotificationItem], nextCursor: PushNotificationCursor?)
3738
case resetPagination
3839
case setHasMore(Bool)
40+
case syncNotifications([PushNotificationItem], nextCursor: PushNotificationCursor?, hasMore: Bool)
3941
case toggleSortOption
4042
case setTimeFilter(PushNotificationQuery.TimeFilter)
4143
case toggleUnreadOnly
@@ -57,6 +59,7 @@ final class PushNotificationListViewModel: Store {
5759
private let fetchQueryUseCase: FetchPushNotificationQueryUseCase
5860
private let updateQueryUseCase: UpdatePushNotificationQueryUseCase
5961
private var pendingTask: (PushNotificationItem, Int)?
62+
private var cancellable: AnyCancellable?
6063

6164
init(
6265
fetchUseCase: FetchPushNotificationsUseCase,
@@ -95,7 +98,7 @@ final class PushNotificationListViewModel: Store {
9598
case .fetchNotifications, .confirmDelete, .setToast, .setSelectedTodoId, .loadNextPage:
9699
effects = reduceByView(action, state: &state)
97100

98-
case .setLoading, .appendNotifications, .resetPagination, .setHasMore:
101+
case .setLoading, .appendNotifications, .resetPagination, .setHasMore, .syncNotifications:
99102
effects = reduceByRun(action, state: &state)
100103
}
101104

@@ -106,10 +109,14 @@ final class PushNotificationListViewModel: Store {
106109
func run(_ effect: SideEffect) {
107110
switch effect {
108111
case .fetchNotifications(let query, let cursor):
112+
if cursor == nil {
113+
stopObservingNotifications()
114+
}
109115
Task {
110116
do {
111117
defer { send(.setLoading(false)) }
112118
send(.setLoading(true))
119+
let existingCount = cursor == nil ? 0 : self.state.notifications.count
113120

114121
let page = try await fetchUseCase.execute(query, cursor: cursor)
115122

@@ -123,6 +130,10 @@ final class PushNotificationListViewModel: Store {
123130

124131
let hasMore = page.items.count == query.pageSize && page.nextCursor != nil
125132
send(.setHasMore(hasMore))
133+
startObservingNotifications(
134+
query: query,
135+
limit: max(query.pageSize, existingCount + page.items.count)
136+
)
126137
} catch {
127138
send(.setAlert(isPresented: true))
128139
}
@@ -252,6 +263,16 @@ private extension PushNotificationListViewModel {
252263
}
253264
state.notifications.append(contentsOf: filteredNotifications)
254265
state.nextCursor = nextCursor
266+
case .syncNotifications(let notifications, let nextCursor, let hasMore):
267+
let filteredNotifications: [PushNotificationItem]
268+
if let (pendingItem, _) = pendingTask {
269+
filteredNotifications = notifications.filter { $0.id != pendingItem.id }
270+
} else {
271+
filteredNotifications = notifications
272+
}
273+
state.notifications = filteredNotifications
274+
state.nextCursor = nextCursor
275+
state.hasMore = hasMore
255276
default:
256277
break
257278
}
@@ -276,6 +297,33 @@ private extension PushNotificationListViewModel {
276297
state.toastMessage = "실행 취소"
277298
state.showToast = isPresented
278299
}
300+
301+
func startObservingNotifications(
302+
query: PushNotificationQuery,
303+
limit: Int
304+
) {
305+
cancellable = try? fetchUseCase.observe(query, limit: limit)
306+
.receive(on: DispatchQueue.main)
307+
.sink(
308+
receiveCompletion: { [weak self] completion in
309+
guard let self else { return }
310+
if case .failure = completion {
311+
self.send(.setAlert(isPresented: true))
312+
}
313+
},
314+
receiveValue: { [weak self] page in
315+
guard let self else { return }
316+
let items = page.items.map { PushNotificationItem(from: $0) }
317+
let hasMore = items.count == max(query.pageSize, limit) && page.nextCursor != nil
318+
self.send(.syncNotifications(items, nextCursor: page.nextCursor, hasMore: hasMore))
319+
}
320+
)
321+
}
322+
323+
func stopObservingNotifications() {
324+
cancellable?.cancel()
325+
cancellable = nil
326+
}
279327
}
280328

281329
extension PushNotificationQuery.SortOrder {

0 commit comments

Comments
 (0)