From 8f3d38282b6fb42e761715d42fbbee68388fe91a Mon Sep 17 00:00:00 2001 From: opficdev Date: Fri, 27 Feb 2026 10:32:01 +0900 Subject: [PATCH 1/4] =?UTF-8?q?refactor:=20PushNotification=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/{Infra/DTO => Domain/Entity}/PushNotification.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename DevLog/{Infra/DTO => Domain/Entity}/PushNotification.swift (86%) 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 From 8396d7942042cfbc5a4b7c7e280edf76642d8369 Mon Sep 17 00:00:00 2001 From: opficdev Date: Fri, 27 Feb 2026 10:39:48 +0900 Subject: [PATCH 2/4] =?UTF-8?q?refacot:=20Presentation,=20UI=20=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=96=B4=20=EC=9A=A9=20=EB=AA=A8=EB=8D=B8=EC=9D=B8=20?= =?UTF-8?q?PushNotificationItem=EB=A1=9C=20=EB=8C=80=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Structure/PushNotificationItem.swift | 48 +++++++++++++++++++ .../PushNotificationListViewModel.swift | 23 +++++---- .../PushNotificationListView.swift | 18 +++---- 3 files changed, 71 insertions(+), 18 deletions(-) create mode 100644 DevLog/Presentation/Structure/PushNotificationItem.swift diff --git a/DevLog/Presentation/Structure/PushNotificationItem.swift b/DevLog/Presentation/Structure/PushNotificationItem.swift new file mode 100644 index 0000000..ee79eb9 --- /dev/null +++ b/DevLog/Presentation/Structure/PushNotificationItem.swift @@ -0,0 +1,48 @@ +// +// 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 + + private init( + id: String, + title: String, + body: String, + receivedAt: Date, + isRead: Bool, + todoID: String, + todoKind: TodoKind + ) { + self.id = id + self.title = title + self.body = body + self.receivedAt = receivedAt + self.isRead = isRead + self.todoID = todoID + self.todoKind = todoKind + } + + init(from notification: PushNotification) { + self.init( + id: notification.id, + title: notification.title, + body: notification.body, + receivedAt: notification.receivedAt, + isRead: notification.isRead, + todoID: notification.todoID, + todoKind: notification.todoKind + ) + } +} 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") From 936f6a117af53d3fc484d21d472568146e960c75 Mon Sep 17 00:00:00 2001 From: opficdev Date: Fri, 27 Feb 2026 10:44:31 +0900 Subject: [PATCH 3/4] =?UTF-8?q?refactor:=20=EB=8D=B0=EC=9D=B4=ED=84=B0,=20?= =?UTF-8?q?=EC=9D=B8=ED=94=84=EB=9D=BC=20=EB=A0=88=EC=9D=B4=EC=96=B4=20?= =?UTF-8?q?=EB=AA=A8=EB=8D=B8=20=EB=8C=80=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Data/Mapper/PushNotificationMapping.swift | 53 +++++++++++++++++++ .../PushNotificationRepositoryImpl.swift | 30 ++--------- .../Infra/DTO/PushNotificationCursorDTO.swift | 13 +++++ .../DTO/PushNotificationCursorResponse.swift | 13 ----- .../DTO/PushNotificationPageResponse.swift | 2 +- .../Service/PushNotificationService.swift | 8 +-- 6 files changed, 74 insertions(+), 45 deletions(-) create mode 100644 DevLog/Data/Mapper/PushNotificationMapping.swift create mode 100644 DevLog/Infra/DTO/PushNotificationCursorDTO.swift delete mode 100644 DevLog/Infra/DTO/PushNotificationCursorResponse.swift 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/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 ) From d2199abb7832a7d1bcf7b7ca354fd62fb7a5d32f Mon Sep 17 00:00:00 2001 From: opficdev Date: Fri, 27 Feb 2026 10:57:45 +0900 Subject: [PATCH 4/4] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94=20p?= =?UTF-8?q?rivate=20init=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Structure/PinnedTodoItem.swift | 22 +++--------- .../Structure/PushNotificationItem.swift | 34 ++++--------------- .../Presentation/Structure/TodoListItem.swift | 22 +++--------- 3 files changed, 15 insertions(+), 63 deletions(-) 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 index ee79eb9..1d8201e 100644 --- a/DevLog/Presentation/Structure/PushNotificationItem.swift +++ b/DevLog/Presentation/Structure/PushNotificationItem.swift @@ -16,33 +16,13 @@ struct PushNotificationItem: Identifiable, Hashable { let todoID: String let todoKind: TodoKind - private init( - id: String, - title: String, - body: String, - receivedAt: Date, - isRead: Bool, - todoID: String, - todoKind: TodoKind - ) { - self.id = id - self.title = title - self.body = body - self.receivedAt = receivedAt - self.isRead = isRead - self.todoID = todoID - self.todoKind = todoKind - } - init(from notification: PushNotification) { - self.init( - id: notification.id, - title: notification.title, - body: notification.body, - receivedAt: notification.receivedAt, - isRead: notification.isRead, - todoID: notification.todoID, - todoKind: notification.todoKind - ) + 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 } }