Skip to content
Merged
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
53 changes: 53 additions & 0 deletions DevLog/Data/Mapper/PushNotificationMapping.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
30 changes: 3 additions & 27 deletions DevLog/Data/Repository/PushNotificationRepositoryImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

// 푸시 알림 기록 삭제
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import Foundation

struct PushNotification: Identifiable {
struct PushNotification {
let id: String
let title: String
let body: String
Expand Down
13 changes: 13 additions & 0 deletions DevLog/Infra/DTO/PushNotificationCursorDTO.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// PushNotificationCursorDTO.swift
// DevLog
//
// Created by 최윤진 on 2/27/26.
//

import FirebaseFirestore

struct PushNotificationCursorDTO {
let receivedAt: Timestamp
let documentID: String
}
13 changes: 0 additions & 13 deletions DevLog/Infra/DTO/PushNotificationCursorResponse.swift

This file was deleted.

2 changes: 1 addition & 1 deletion DevLog/Infra/DTO/PushNotificationPageResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@

struct PushNotificationPageResponse {
let items: [PushNotificationResponse]
let nextCursor: PushNotificationCursorResponse?
let nextCursor: PushNotificationCursorDTO?
}
8 changes: 4 additions & 4 deletions DevLog/Infra/Service/PushNotificationService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand All @@ -115,7 +115,7 @@ final class PushNotificationService {

if let cursor {
firestoreQuery = firestoreQuery.start(after: [
Timestamp(date: cursor.receivedAt),
cursor.receivedAt,
cursor.documentID
])
}
Expand All @@ -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
)
Expand Down
22 changes: 4 additions & 18 deletions DevLog/Presentation/Structure/PinnedTodoItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
28 changes: 28 additions & 0 deletions DevLog/Presentation/Structure/PushNotificationItem.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
22 changes: 4 additions & 18 deletions DevLog/Presentation/Structure/TodoListItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
23 changes: 14 additions & 9 deletions DevLog/Presentation/ViewModel/PushNotificationListViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ""
Expand All @@ -18,35 +18,35 @@ 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?
}

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)
}

Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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 {
Expand Down
18 changes: 9 additions & 9 deletions DevLog/UI/PushNotification/PushNotificationListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
}
Expand All @@ -213,17 +213,17 @@ 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)
}
}
.swipeActions(edge: .trailing) {
Button(
role: .destructive,
action: {
viewModel.send(.deleteNotification(notification))
viewModel.send(.deleteNotification(item))
}
) {
Image(systemName: "trash")
Expand Down