diff --git a/DevLog/Data/Mapper/PushNotificationMapping.swift b/DevLog/Data/Mapper/PushNotificationMapping.swift new file mode 100644 index 0000000..66150e9 --- /dev/null +++ b/DevLog/Data/Mapper/PushNotificationMapping.swift @@ -0,0 +1,53 @@ +// +// PushNotificationMapping.swift +// DevLog +// +// Created by 최윤진 on 2/27/26. +// + +import FirebaseFirestore + +extension PushNotificationResponse { + func toDomain() throws -> PushNotification { + guard let id = self.id else { + throw DataError.invalidData("PushNotificationResponse.id is nil") + } + guard let todoKind = TodoKind(rawValue: self.todoKind) else { + throw DataError.invalidData("PushNotificationResponse.todoKind is invalid: \(self.todoKind)") + } + + return PushNotification( + id: id, + title: self.title, + body: self.body, + receivedAt: self.receivedAt.dateValue(), + isRead: self.isRead, + todoID: self.todoID, + todoKind: todoKind + ) + } +} + +extension PushNotificationCursorDTO { + func toDomain() -> PushNotificationCursor { + PushNotificationCursor( + receivedAt: self.receivedAt.dateValue(), + documentID: self.documentID + ) + } + + static func fromDomain(_ cursor: PushNotificationCursor) -> Self { + PushNotificationCursorDTO( + receivedAt: Timestamp(date: cursor.receivedAt), + documentID: cursor.documentID + ) + } +} + +extension PushNotificationPageResponse { + func toDomain() throws -> PushNotificationPage { + let items = try self.items.map { try $0.toDomain() } + let nextCursor = self.nextCursor?.toDomain() + return PushNotificationPage(items: items, nextCursor: nextCursor) + } +} diff --git a/DevLog/Data/Repository/PushNotificationRepositoryImpl.swift b/DevLog/Data/Repository/PushNotificationRepositoryImpl.swift index 586f352..fd8ac95 100644 --- a/DevLog/Data/Repository/PushNotificationRepositoryImpl.swift +++ b/DevLog/Data/Repository/PushNotificationRepositoryImpl.swift @@ -36,33 +36,9 @@ final class PushNotificationRepositoryImpl: PushNotificationRepository { _ query: PushNotificationQuery, cursor: PushNotificationCursor? ) async throws -> PushNotificationPage { - let response = try await service.requestNotifications(query, cursor: cursor) - - let items: [PushNotification] = response.items.compactMap { dto in - guard - let id = dto.id, - let todoKind = TodoKind(rawValue: dto.todoKind) - else { return nil } - - return PushNotification( - id: id, - title: dto.title, - body: dto.body, - receivedAt: dto.receivedAt.dateValue(), - isRead: dto.isRead, - todoID: dto.todoID, - todoKind: todoKind - ) - } - - let nextCursor = response.nextCursor.map { cursor in - PushNotificationCursor( - receivedAt: cursor.receivedAt.dateValue(), - documentID: cursor.documentID - ) - } - - return PushNotificationPage(items: items, nextCursor: nextCursor) + let cursorDTO = cursor.map { PushNotificationCursorDTO.fromDomain($0) } + let response = try await service.requestNotifications(query, cursor: cursorDTO) + return try response.toDomain() } // 푸시 알림 기록 삭제 diff --git a/DevLog/Infra/DTO/PushNotification.swift b/DevLog/Domain/Entity/PushNotification.swift similarity index 86% rename from DevLog/Infra/DTO/PushNotification.swift rename to DevLog/Domain/Entity/PushNotification.swift index 1929590..9989378 100644 --- a/DevLog/Infra/DTO/PushNotification.swift +++ b/DevLog/Domain/Entity/PushNotification.swift @@ -7,7 +7,7 @@ import Foundation -struct PushNotification: Identifiable { +struct PushNotification { let id: String let title: String let body: String diff --git a/DevLog/Infra/DTO/PushNotificationCursorDTO.swift b/DevLog/Infra/DTO/PushNotificationCursorDTO.swift new file mode 100644 index 0000000..0d2aaac --- /dev/null +++ b/DevLog/Infra/DTO/PushNotificationCursorDTO.swift @@ -0,0 +1,13 @@ +// +// PushNotificationCursorDTO.swift +// DevLog +// +// Created by 최윤진 on 2/27/26. +// + +import FirebaseFirestore + +struct PushNotificationCursorDTO { + let receivedAt: Timestamp + let documentID: String +} diff --git a/DevLog/Infra/DTO/PushNotificationCursorResponse.swift b/DevLog/Infra/DTO/PushNotificationCursorResponse.swift deleted file mode 100644 index 57c5dd8..0000000 --- a/DevLog/Infra/DTO/PushNotificationCursorResponse.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// PushNotificationCursorResponse.swift -// DevLog -// -// Created by opfic on 2/18/26. -// - -import FirebaseFirestore - -struct PushNotificationCursorResponse { - let receivedAt: Timestamp - let documentID: String -} diff --git a/DevLog/Infra/DTO/PushNotificationPageResponse.swift b/DevLog/Infra/DTO/PushNotificationPageResponse.swift index a6efe06..572d740 100644 --- a/DevLog/Infra/DTO/PushNotificationPageResponse.swift +++ b/DevLog/Infra/DTO/PushNotificationPageResponse.swift @@ -7,5 +7,5 @@ struct PushNotificationPageResponse { let items: [PushNotificationResponse] - let nextCursor: PushNotificationCursorResponse? + let nextCursor: PushNotificationCursorDTO? } diff --git a/DevLog/Infra/Service/PushNotificationService.swift b/DevLog/Infra/Service/PushNotificationService.swift index 0b496d3..4a3f722 100644 --- a/DevLog/Infra/Service/PushNotificationService.swift +++ b/DevLog/Infra/Service/PushNotificationService.swift @@ -91,7 +91,7 @@ final class PushNotificationService { /// 푸시 알림 기록 요청 func requestNotifications( _ query: PushNotificationQuery, - cursor: PushNotificationCursor? + cursor: PushNotificationCursorDTO? ) async throws -> PushNotificationPageResponse { guard let uid = Auth.auth().currentUser?.uid else { throw AuthError.notAuthenticated } @@ -115,7 +115,7 @@ final class PushNotificationService { if let cursor { firestoreQuery = firestoreQuery.start(after: [ - Timestamp(date: cursor.receivedAt), + cursor.receivedAt, cursor.documentID ]) } @@ -128,12 +128,12 @@ final class PushNotificationService { try document.data(as: PushNotificationResponse.self) } - let nextCursor: PushNotificationCursorResponse? = snapshot.documents.last.map { document in + let nextCursor: PushNotificationCursorDTO? = snapshot.documents.last.map { document in guard let receivedAt = document.data()["receivedAt"] as? Timestamp else { return nil } - return PushNotificationCursorResponse( + return PushNotificationCursorDTO( receivedAt: receivedAt, documentID: document.documentID ) diff --git a/DevLog/Presentation/Structure/PinnedTodoItem.swift b/DevLog/Presentation/Structure/PinnedTodoItem.swift index cbe4f1e..d1d034f 100644 --- a/DevLog/Presentation/Structure/PinnedTodoItem.swift +++ b/DevLog/Presentation/Structure/PinnedTodoItem.swift @@ -13,24 +13,10 @@ struct PinnedTodoItem: Identifiable, Hashable { let dueDate: Date? let kind: TodoKind - private init( - id: String, - title: String, - dueDate: Date?, - kind: TodoKind - ) { - self.id = id - self.title = title - self.dueDate = dueDate - self.kind = kind - } - init(from todo: Todo) { - self.init( - id: todo.id, - title: todo.title, - dueDate: todo.dueDate, - kind: todo.kind - ) + self.id = todo.id + self.title = todo.title + self.dueDate = todo.dueDate + self.kind = todo.kind } } diff --git a/DevLog/Presentation/Structure/PushNotificationItem.swift b/DevLog/Presentation/Structure/PushNotificationItem.swift new file mode 100644 index 0000000..1d8201e --- /dev/null +++ b/DevLog/Presentation/Structure/PushNotificationItem.swift @@ -0,0 +1,28 @@ +// +// PushNotificationItem.swift +// DevLog +// +// Created by 최윤진 on 2/27/26. +// + +import Foundation + +struct PushNotificationItem: Identifiable, Hashable { + let id: String + let title: String + let body: String + let receivedAt: Date + var isRead: Bool + let todoID: String + let todoKind: TodoKind + + init(from notification: PushNotification) { + self.id = notification.id + self.title = notification.title + self.body = notification.body + self.receivedAt = notification.receivedAt + self.isRead = notification.isRead + self.todoID = notification.todoID + self.todoKind = notification.todoKind + } +} diff --git a/DevLog/Presentation/Structure/TodoListItem.swift b/DevLog/Presentation/Structure/TodoListItem.swift index 5885f53..ac00d9f 100644 --- a/DevLog/Presentation/Structure/TodoListItem.swift +++ b/DevLog/Presentation/Structure/TodoListItem.swift @@ -13,24 +13,10 @@ struct TodoListItem: Identifiable, Hashable { let tags: [String] let isPinned: Bool - private init( - id: String, - title: String, - tags: [String], - isPinned: Bool - ) { - self.id = id - self.title = title - self.tags = tags - self.isPinned = isPinned - } - init(from todo: Todo) { - self.init( - id: todo.id, - title: todo.title, - tags: todo.tags, - isPinned: todo.isPinned - ) + self.id = todo.id + self.title = todo.title + self.tags = todo.tags + self.isPinned = todo.isPinned } } diff --git a/DevLog/Presentation/ViewModel/PushNotificationListViewModel.swift b/DevLog/Presentation/ViewModel/PushNotificationListViewModel.swift index 9ae123c..91497b4 100644 --- a/DevLog/Presentation/ViewModel/PushNotificationListViewModel.swift +++ b/DevLog/Presentation/ViewModel/PushNotificationListViewModel.swift @@ -9,7 +9,7 @@ import Foundation final class PushNotificationListViewModel: Store { struct State { - var notifications: [PushNotification] = [] + var notifications: [PushNotificationItem] = [] var showAlert: Bool = false var showToast: Bool = false var alertTitle: String = "" @@ -18,7 +18,7 @@ final class PushNotificationListViewModel: Store { var isLoading: Bool = false var hasMore: Bool = false var nextCursor: PushNotificationCursor? - var pendingTask: (PushNotification, Int)? + var pendingTask: (PushNotificationItem, Int)? var query: PushNotificationQuery var selectedTodoID: TodoIDItem? } @@ -26,27 +26,27 @@ final class PushNotificationListViewModel: Store { enum Action { case fetchNotifications case loadNextPage - case deleteNotification(PushNotification) - case toggleRead(PushNotification) + case deleteNotification(PushNotificationItem) + case toggleRead(PushNotificationItem) case undoDelete case confirmDelete case setAlert(isPresented: Bool) case setToast(isPresented: Bool) case setLoading(Bool) - case appendNotifications([PushNotification], nextCursor: PushNotificationCursor?) + case appendNotifications([PushNotificationItem], nextCursor: PushNotificationCursor?) case resetPagination case setHasMore(Bool) case toggleSortOption case setTimeFilter(PushNotificationQuery.TimeFilter) case toggleUnreadOnly case resetFilters - case tapNotification(PushNotification) + case tapNotification(PushNotificationItem) case setSelectedTodoID(TodoIDItem?) } enum SideEffect { case fetchNotifications(PushNotificationQuery, cursor: PushNotificationCursor?) - case delete(PushNotification) + case delete(PushNotificationItem) case toggleRead(String) } @@ -113,7 +113,12 @@ final class PushNotificationListViewModel: Store { let page = try await fetchUseCase.execute(query, cursor: cursor) if cursor == nil { send(.resetPagination) } - send(.appendNotifications(page.items, nextCursor: page.nextCursor)) + send( + .appendNotifications( + page.items.map { PushNotificationItem(from: $0) }, + nextCursor: page.nextCursor + ) + ) let hasMore = page.items.count == query.pageSize && page.nextCursor != nil send(.setHasMore(hasMore)) @@ -238,7 +243,7 @@ private extension PushNotificationListViewModel { state.notifications = [] state.nextCursor = nil case .appendNotifications(let notifications, let nextCursor): - let filteredNotifications: [PushNotification] + let filteredNotifications: [PushNotificationItem] if let (pendingItem, _) = state.pendingTask { filteredNotifications = notifications.filter { $0.id != pendingItem.id } } else { diff --git a/DevLog/UI/PushNotification/PushNotificationListView.swift b/DevLog/UI/PushNotification/PushNotificationListView.swift index d2ec1af..ca0ec84 100644 --- a/DevLog/UI/PushNotification/PushNotificationListView.swift +++ b/DevLog/UI/PushNotification/PushNotificationListView.swift @@ -173,10 +173,10 @@ struct PushNotificationListView: View { } // swiftlint:disable function_body_length - private func notificationRow(_ notification: PushNotification) -> some View { + private func notificationRow(_ item: PushNotificationItem) -> some View { HStack { VStack { - let todoKind = notification.todoKind + let todoKind = item.todoKind RoundedRectangle(cornerRadius: 8) .fill(todoKind.color) .frame(width: sceneWidth * 0.08, height: sceneWidth * 0.08) @@ -188,14 +188,14 @@ struct PushNotificationListView: View { Circle() .fill(Color.blue) .frame(width: 8, height: 8) - .opacity(notification.isRead ? 0 : 1) + .opacity(item.isRead ? 0 : 1) } VStack(alignment: .leading, spacing: 5) { - Text(notification.title) + Text(item.title) .font(.headline) .lineLimit(1) - Text(notification.body) + Text(item.body) .font(.subheadline) .foregroundStyle(Color.gray) .lineLimit(1) @@ -204,7 +204,7 @@ struct PushNotificationListView: View { Spacer() TimelineView(.periodic(from: .now, by: 1.0)) { context in - Text(timeAgoText(from: notification.receivedAt, now: context.date)) + Text(timeAgoText(from: item.receivedAt, now: context.date)) .font(.caption2) .foregroundStyle(Color.gray) } @@ -213,9 +213,9 @@ struct PushNotificationListView: View { .contentShape(.rect) .swipeActions(edge: .leading) { Button { - viewModel.send(.toggleRead(notification)) + viewModel.send(.toggleRead(item)) } label: { - Image(systemName: "checkmark.circle\(notification.isRead ? ".badge.xmark" : "")") + Image(systemName: "checkmark.circle\(item.isRead ? ".badge.xmark" : "")") .tint(.blue) } } @@ -223,7 +223,7 @@ struct PushNotificationListView: View { Button( role: .destructive, action: { - viewModel.send(.deleteNotification(notification)) + viewModel.send(.deleteNotification(item)) } ) { Image(systemName: "trash")