diff --git a/DevLog/Infra/DTO/AppleAuthResponse.swift b/DevLog/Data/DTO/AppleAuthResponse.swift similarity index 100% rename from DevLog/Infra/DTO/AppleAuthResponse.swift rename to DevLog/Data/DTO/AppleAuthResponse.swift diff --git a/DevLog/Infra/DTO/AuthDataResponse.swift b/DevLog/Data/DTO/AuthDataResponse.swift similarity index 100% rename from DevLog/Infra/DTO/AuthDataResponse.swift rename to DevLog/Data/DTO/AuthDataResponse.swift diff --git a/DevLog/Infra/DTO/NotificationKind.swift b/DevLog/Data/DTO/NotificationKind.swift similarity index 100% rename from DevLog/Infra/DTO/NotificationKind.swift rename to DevLog/Data/DTO/NotificationKind.swift diff --git a/DevLog/Infra/DTO/PushNotificationCursorDTO.swift b/DevLog/Data/DTO/PushNotificationCursorDTO.swift similarity index 74% rename from DevLog/Infra/DTO/PushNotificationCursorDTO.swift rename to DevLog/Data/DTO/PushNotificationCursorDTO.swift index 0d2aaac3..ea67ce07 100644 --- a/DevLog/Infra/DTO/PushNotificationCursorDTO.swift +++ b/DevLog/Data/DTO/PushNotificationCursorDTO.swift @@ -5,9 +5,9 @@ // Created by 최윤진 on 2/27/26. // -import FirebaseFirestore +import Foundation struct PushNotificationCursorDTO { - let receivedAt: Timestamp + let receivedAt: Date let documentID: String } diff --git a/DevLog/Infra/DTO/PushNotificationPageResponse.swift b/DevLog/Data/DTO/PushNotificationPageResponse.swift similarity index 100% rename from DevLog/Infra/DTO/PushNotificationPageResponse.swift rename to DevLog/Data/DTO/PushNotificationPageResponse.swift diff --git a/DevLog/Infra/DTO/PushNotificationResponse.swift b/DevLog/Data/DTO/PushNotificationResponse.swift similarity index 61% rename from DevLog/Infra/DTO/PushNotificationResponse.swift rename to DevLog/Data/DTO/PushNotificationResponse.swift index 2ae7a3f0..3a791b75 100644 --- a/DevLog/Infra/DTO/PushNotificationResponse.swift +++ b/DevLog/Data/DTO/PushNotificationResponse.swift @@ -5,13 +5,13 @@ // Created by 최윤진 on 2/10/26. // -import FirebaseFirestore +import Foundation -struct PushNotificationResponse: Decodable { - @DocumentID var id: String? +struct PushNotificationResponse { + let id: String let title: String let body: String - let receivedAt: Timestamp + let receivedAt: Date let isRead: Bool let todoID: String let todoKind: String diff --git a/DevLog/Data/DTO/TodoCursorDTO.swift b/DevLog/Data/DTO/TodoCursorDTO.swift new file mode 100644 index 00000000..bf99f895 --- /dev/null +++ b/DevLog/Data/DTO/TodoCursorDTO.swift @@ -0,0 +1,13 @@ +// +// TodoCursorDTO.swift +// DevLog +// +// Created by opfic on 2/21/26. +// + +import Foundation + +struct TodoCursorDTO { + let createdAt: Date + let documentID: String +} diff --git a/DevLog/Data/DTO/TodoDTO.swift b/DevLog/Data/DTO/TodoDTO.swift new file mode 100644 index 00000000..4402e028 --- /dev/null +++ b/DevLog/Data/DTO/TodoDTO.swift @@ -0,0 +1,37 @@ +// +// TodoDTO.swift +// DevLog +// +// Created by 최윤진 on 12/14/25. +// + +import Foundation + +struct TodoRequest: Encodable { + let id: String + let isPinned: Bool + let isCompleted: Bool + let isChecked: Bool + let title: String + let content: String + let createdAt: Date + let updatedAt: Date + let dueDate: Date? + let tags: [String] + let kind: TodoKind + +} + +struct TodoResponse { + let id: String + let isPinned: Bool + let isCompleted: Bool + let isChecked: Bool + let title: String + let content: String + let createdAt: Date + let updatedAt: Date + let dueDate: Date? + let tags: [String] + let kind: String +} diff --git a/DevLog/Infra/DTO/TodoPageResponse.swift b/DevLog/Data/DTO/TodoPageResponse.swift similarity index 77% rename from DevLog/Infra/DTO/TodoPageResponse.swift rename to DevLog/Data/DTO/TodoPageResponse.swift index bc71b3a6..56faf1ec 100644 --- a/DevLog/Infra/DTO/TodoPageResponse.swift +++ b/DevLog/Data/DTO/TodoPageResponse.swift @@ -7,5 +7,5 @@ struct TodoPageResponse { let items: [TodoResponse] - let nextCursor: TodoCursorResponse? + let nextCursor: TodoCursorDTO? } diff --git a/DevLog/Infra/DTO/UserProfileResponse.swift b/DevLog/Data/DTO/UserProfileResponse.swift similarity index 100% rename from DevLog/Infra/DTO/UserProfileResponse.swift rename to DevLog/Data/DTO/UserProfileResponse.swift diff --git a/DevLog/Infra/DTO/WebPageDTO.swift b/DevLog/Data/DTO/WebPageDTO.swift similarity index 77% rename from DevLog/Infra/DTO/WebPageDTO.swift rename to DevLog/Data/DTO/WebPageDTO.swift index 9da9998e..980ec255 100644 --- a/DevLog/Infra/DTO/WebPageDTO.swift +++ b/DevLog/Data/DTO/WebPageDTO.swift @@ -5,7 +5,7 @@ // Created by 최윤진 on 2/9/26. // -import FirebaseFirestore +import Foundation struct WebPageRequest: Encodable { let title: String @@ -14,8 +14,8 @@ struct WebPageRequest: Encodable { let imageURL: String } -struct WebPageResponse: Decodable { - @DocumentID var id: String? +struct WebPageResponse { + let id: String let title: String let url: String let displayURL: String diff --git a/DevLog/Infra/DTO/WebPageMetadataResponse.swift b/DevLog/Data/DTO/WebPageMetadataResponse.swift similarity index 100% rename from DevLog/Infra/DTO/WebPageMetadataResponse.swift rename to DevLog/Data/DTO/WebPageMetadataResponse.swift diff --git a/DevLog/Data/Mapper/PushNotificationMapping.swift b/DevLog/Data/Mapper/PushNotificationMapping.swift index 66150e98..617aeba6 100644 --- a/DevLog/Data/Mapper/PushNotificationMapping.swift +++ b/DevLog/Data/Mapper/PushNotificationMapping.swift @@ -5,13 +5,8 @@ // 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)") } @@ -20,7 +15,7 @@ extension PushNotificationResponse { id: id, title: self.title, body: self.body, - receivedAt: self.receivedAt.dateValue(), + receivedAt: self.receivedAt, isRead: self.isRead, todoID: self.todoID, todoKind: todoKind @@ -31,14 +26,14 @@ extension PushNotificationResponse { extension PushNotificationCursorDTO { func toDomain() -> PushNotificationCursor { PushNotificationCursor( - receivedAt: self.receivedAt.dateValue(), + receivedAt: self.receivedAt, documentID: self.documentID ) } static func fromDomain(_ cursor: PushNotificationCursor) -> Self { PushNotificationCursorDTO( - receivedAt: Timestamp(date: cursor.receivedAt), + receivedAt: cursor.receivedAt, documentID: cursor.documentID ) } diff --git a/DevLog/Data/Mapper/TodoMapping.swift b/DevLog/Data/Mapper/TodoMapping.swift index 77cef84d..9cb8dbf8 100644 --- a/DevLog/Data/Mapper/TodoMapping.swift +++ b/DevLog/Data/Mapper/TodoMapping.swift @@ -5,8 +5,6 @@ // Created by 최윤진 on 2/19/26. // -import FirebaseFirestore - extension TodoRequest { static func fromDomain(_ entity: Todo) -> Self { TodoRequest( @@ -27,9 +25,6 @@ extension TodoRequest { extension TodoResponse { func toDomain() throws -> Todo { - guard let id = self.id else { - throw DataError.invalidData("TodoResponse.id is nil") - } guard let kind = TodoKind(rawValue: self.kind) else { throw DataError.invalidData("TodoResponse.kind is invalid: \(self.kind)") } @@ -50,17 +45,17 @@ extension TodoResponse { } } -extension TodoCursorResponse { +extension TodoCursorDTO { func toDomain() -> TodoCursor { TodoCursor( - createdAt: createdAt.dateValue(), + createdAt: createdAt, documentID: documentID ) } static func fromDomain(_ cursor: TodoCursor) -> Self { - TodoCursorResponse( - createdAt: Timestamp(date: cursor.createdAt), + TodoCursorDTO( + createdAt: cursor.createdAt, documentID: cursor.documentID ) } diff --git a/DevLog/Data/Protocol/Dictionaryable.swift b/DevLog/Data/Protocol/Dictionaryable.swift deleted file mode 100644 index fd7e1c8b..00000000 --- a/DevLog/Data/Protocol/Dictionaryable.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// Dictionaryable.swift -// DevLog -// -// Created by 최윤진 on 12/14/25. -// - -import FirebaseFirestore - -protocol Dictionaryable: Encodable { - func toDictionary() -> [String: Any] -} - -extension Dictionaryable { - func toDictionary() -> [String: Any] { - let encoder = Firestore.Encoder() - guard var dictionary = try? encoder.encode(self) else { return [:] } - - let mirror = Mirror(reflecting: self) - for child in mirror.children { - guard let key = child.label else { continue } - if isNilValue(child.value) { - dictionary[key] = NSNull() - } - } - - dictionary.removeValue(forKey: "id") - return dictionary - } - - private func isNilValue(_ value: Any) -> Bool { - let mirror = Mirror(reflecting: value) - return mirror.displayStyle == .optional && mirror.children.isEmpty - } -} diff --git a/DevLog/Data/Repository/TodoRepositoryImpl.swift b/DevLog/Data/Repository/TodoRepositoryImpl.swift index 871ba832..d020ba74 100644 --- a/DevLog/Data/Repository/TodoRepositoryImpl.swift +++ b/DevLog/Data/Repository/TodoRepositoryImpl.swift @@ -15,7 +15,7 @@ final class TodoRepositoryImpl: TodoRepository { } func fetchTodos(_ query: TodoQuery, cursor: TodoCursor?) async throws -> TodoPage { - let responseCursor = cursor.map { TodoCursorResponse.fromDomain($0) } + let responseCursor = cursor.map { TodoCursorDTO.fromDomain($0) } let response = try await todoService.fetchTodos(query, cursor: responseCursor) return try response.toDomain() } diff --git a/DevLog/Data/Protocol/UserPreferencesRepository.swift b/DevLog/Domain/Protocol/UserPreferencesRepository.swift similarity index 100% rename from DevLog/Data/Protocol/UserPreferencesRepository.swift rename to DevLog/Domain/Protocol/UserPreferencesRepository.swift diff --git a/DevLog/Infra/DTO/TodoCursorResponse.swift b/DevLog/Infra/DTO/TodoCursorResponse.swift deleted file mode 100644 index 6d108ca4..00000000 --- a/DevLog/Infra/DTO/TodoCursorResponse.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// TodoCursorResponse.swift -// DevLog -// -// Created by opfic on 2/21/26. -// - -import FirebaseFirestore - -struct TodoCursorResponse { - let createdAt: Timestamp - let documentID: String -} diff --git a/DevLog/Infra/DTO/TodoDTO.swift b/DevLog/Infra/DTO/TodoDTO.swift deleted file mode 100644 index f45b0fc0..00000000 --- a/DevLog/Infra/DTO/TodoDTO.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// TodoDTO.swift -// DevLog -// -// Created by 최윤진 on 12/14/25. -// - -import Foundation -import FirebaseFirestore - -struct TodoRequest: Dictionaryable { - let id: String - let isPinned: Bool - let isCompleted: Bool - let isChecked: Bool - let title: String - let content: String - let createdAt: Date - let updatedAt: Date - let dueDate: Date? - let tags: [String] - let kind: TodoKind - -} - -struct TodoResponse: Decodable { - @DocumentID var id: String? - let isPinned: Bool - let isCompleted: Bool - let isChecked: Bool - let title: String - let content: String - let createdAt: Date - let updatedAt: Date - let dueDate: Date? - let tags: [String] - let kind: String - - init?(from snapshot: QueryDocumentSnapshot) { - self.init(documentID: snapshot.documentID, data: snapshot.data()) - } - - init?(from snapshot: DocumentSnapshot) { - guard let data = snapshot.data() else { return nil } - self.init(documentID: snapshot.documentID, data: data) - } - - private init?(documentID: String, data: [String: Any]) { - guard - let id = documentID as String?, - let isPinned = data["isPinned"] as? Bool, - let isCompleted = data["isCompleted"] as? Bool, - let isChecked = data["isChecked"] as? Bool, - let title = data["title"] as? String, - let content = data["content"] as? String, - let createdAtTimestamp = data["createdAt"] as? Timestamp, - let updatedAtTimestamp = data["updatedAt"] as? Timestamp, - let tags = data["tags"] as? [String], - let kind = data["kind"] as? String else { - return nil - } - self.id = id - self.isPinned = isPinned - self.isCompleted = isCompleted - self.isChecked = isChecked - self.title = title - self.content = content - self.createdAt = createdAtTimestamp.dateValue() - self.updatedAt = updatedAtTimestamp.dateValue() - if let dueDateTimestamp = data["dueDate"] as? Timestamp { - self.dueDate = dueDateTimestamp.dateValue() - } else { - self.dueDate = nil - } - self.tags = tags - self.kind = kind - } - -} diff --git a/DevLog/Infra/Extension/FirebaseAuthUser.swift b/DevLog/Infra/Extension/FirebaseAuthUser.swift index a2b3e2d2..7aea4baa 100644 --- a/DevLog/Infra/Extension/FirebaseAuthUser.swift +++ b/DevLog/Infra/Extension/FirebaseAuthUser.swift @@ -9,7 +9,7 @@ import Foundation import FirebaseAuth extension FirebaseAuth.User { - func toResponse( + func makeResponse( providerID: AuthProviderID, fcmToken: String, accessToken: String? = nil diff --git a/DevLog/Infra/Service/PushNotificationService.swift b/DevLog/Infra/Service/PushNotificationService.swift index 4a3f722f..1a0dac20 100644 --- a/DevLog/Infra/Service/PushNotificationService.swift +++ b/DevLog/Infra/Service/PushNotificationService.swift @@ -115,7 +115,7 @@ final class PushNotificationService { if let cursor { firestoreQuery = firestoreQuery.start(after: [ - cursor.receivedAt, + Timestamp(date: cursor.receivedAt), cursor.documentID ]) } @@ -124,17 +124,15 @@ final class PushNotificationService { .limit(to: query.pageSize) .getDocuments() - let items = try snapshot.documents.compactMap { document in - try document.data(as: PushNotificationResponse.self) - } + let items = snapshot.documents.compactMap { makeResponse(from: $0) } let nextCursor: PushNotificationCursorDTO? = snapshot.documents.last.map { document in - guard let receivedAt = document.data()["receivedAt"] as? Timestamp else { + guard let receivedAt = document.data()[NotificationFieldKey.receivedAt.rawValue] as? Timestamp else { return nil } return PushNotificationCursorDTO( - receivedAt: receivedAt, + receivedAt: receivedAt.dateValue(), documentID: document.documentID ) } ?? nil @@ -177,3 +175,37 @@ final class PushNotificationService { logger.info("Successfully toggled notification read") } } + +private extension PushNotificationService { + func makeResponse(from snapshot: QueryDocumentSnapshot) -> PushNotificationResponse? { + let data = snapshot.data() + guard + let title = data[NotificationFieldKey.title.rawValue] as? String, + let body = data[NotificationFieldKey.body.rawValue] as? String, + let receivedAt = data[NotificationFieldKey.receivedAt.rawValue] as? Timestamp, + let isRead = data[NotificationFieldKey.isRead.rawValue] as? Bool, + let todoID = data[NotificationFieldKey.todoID.rawValue] as? String, + let todoKind = data[NotificationFieldKey.todoKind.rawValue] as? String else { + return nil + } + + return PushNotificationResponse( + id: snapshot.documentID, + title: title, + body: body, + receivedAt: receivedAt.dateValue(), + isRead: isRead, + todoID: todoID, + todoKind: todoKind + ) + } + + enum NotificationFieldKey: String { + case title + case body + case receivedAt + case isRead + case todoID + case todoKind + } +} diff --git a/DevLog/Infra/Service/SocialLogin/AppleAuthenticationService.swift b/DevLog/Infra/Service/SocialLogin/AppleAuthenticationService.swift index c8b9df69..d7afdb9d 100644 --- a/DevLog/Infra/Service/SocialLogin/AppleAuthenticationService.swift +++ b/DevLog/Infra/Service/SocialLogin/AppleAuthenticationService.swift @@ -81,7 +81,7 @@ final class AppleAuthenticationService: AuthenticationService { let fcmToken = try await messaging.token() logger.info("Successfully signed in with Apple") - return result.user.toResponse(providerID: .apple, fcmToken: fcmToken) + return result.user.makeResponse(providerID: .apple, fcmToken: fcmToken) } catch { logger.error("Failed to sign in with Apple", error: error) throw error diff --git a/DevLog/Infra/Service/SocialLogin/GithubAuthenticationService.swift b/DevLog/Infra/Service/SocialLogin/GithubAuthenticationService.swift index 5a00f15b..340ecbb8 100644 --- a/DevLog/Infra/Service/SocialLogin/GithubAuthenticationService.swift +++ b/DevLog/Infra/Service/SocialLogin/GithubAuthenticationService.swift @@ -56,7 +56,7 @@ final class GithubAuthenticationService: NSObject, AuthenticationService { let fcmToken = try await messaging.token() logger.info("Successfully signed in with GitHub") - return result.user.toResponse( + return result.user.makeResponse( providerID: .gitHub, fcmToken: fcmToken, accessToken: accessToken diff --git a/DevLog/Infra/Service/SocialLogin/GoogleAuthenticationService.swift b/DevLog/Infra/Service/SocialLogin/GoogleAuthenticationService.swift index e297eff6..31e99b07 100644 --- a/DevLog/Infra/Service/SocialLogin/GoogleAuthenticationService.swift +++ b/DevLog/Infra/Service/SocialLogin/GoogleAuthenticationService.swift @@ -54,7 +54,7 @@ final class GoogleAuthenticationService: AuthenticationService { let fcmToken = try await messaging.token() logger.info("Successfully signed in with Google") - return result.user.toResponse(providerID: .google, fcmToken: fcmToken) + return result.user.makeResponse(providerID: .google, fcmToken: fcmToken) } catch { logger.error("Failed to sign in with Google", error: error) throw error diff --git a/DevLog/Infra/Service/TodoService.swift b/DevLog/Infra/Service/TodoService.swift index 6dafad9a..98668d9c 100644 --- a/DevLog/Infra/Service/TodoService.swift +++ b/DevLog/Infra/Service/TodoService.swift @@ -10,11 +10,12 @@ import FirebaseFirestore final class TodoService { private let store = Firestore.firestore() + private let encoder = Firestore.Encoder() private let logger = Logger(category: "TodoService") func fetchTodos( _ query: TodoQuery, - cursor: TodoCursorResponse? + cursor: TodoCursorDTO? ) async throws -> TodoPageResponse { guard let uid = Auth.auth().currentUser?.uid else { throw AuthError.notAuthenticated } @@ -41,7 +42,7 @@ final class TodoService { if trimmedKeyword.isEmpty { if let cursor { firestoreQuery = firestoreQuery.start(after: [ - cursor.createdAt, + Timestamp(date: cursor.createdAt), cursor.documentID ]) } @@ -50,15 +51,15 @@ final class TodoService { .limit(to: query.pageSize) .getDocuments() - let items = snapshot.documents.compactMap { TodoResponse(from: $0) } + let items = snapshot.documents.compactMap { makeResponse(from: $0) } - let nextCursor: TodoCursorResponse? = snapshot.documents.last.flatMap { document in - guard let createdAt = document.data()["createdAt"] as? Timestamp else { + let nextCursor: TodoCursorDTO? = snapshot.documents.last.flatMap { document in + guard let createdAt = document.data()[TodoFieldKey.createdAt.rawValue] as? Timestamp else { return nil } - return TodoCursorResponse( - createdAt: createdAt, + return TodoCursorDTO( + createdAt: createdAt.dateValue(), documentID: document.documentID ) } @@ -67,7 +68,7 @@ final class TodoService { } let snapshot = try await firestoreQuery.getDocuments() - let todos = snapshot.documents.compactMap { TodoResponse(from: $0) } + let todos = snapshot.documents.compactMap { makeResponse(from: $0) } let filtered = todos.filter { todo in todo.title.localizedCaseInsensitiveContains(trimmedKeyword) @@ -86,7 +87,12 @@ final class TodoService { do { let collection = store.collection("users/\(uid)/todoLists/") let docRef = collection.document(request.id) - try await docRef.setData(request.toDictionary(), merge: true) + var data = try encoder.encode(request) + data.removeValue(forKey: TodoFieldKey.id.rawValue) + if request.dueDate == nil { + data[TodoFieldKey.dueDate.rawValue] = NSNull() + } + try await docRef.setData(data, merge: true) logger.info("Successfully upserted todo") } catch { @@ -120,7 +126,7 @@ final class TodoService { do { let docRef = store.collection("users/\(uid)/todoLists/").document(todoID) let snapshot = try await docRef.getDocument() - guard snapshot.exists, let todo = TodoResponse(from: snapshot) else { + guard snapshot.exists, let todo = makeResponse(from: snapshot) else { throw FirestoreError.dataNotFound("Todo") } @@ -132,3 +138,60 @@ final class TodoService { } } } + +private extension TodoService { + func makeResponse(from snapshot: QueryDocumentSnapshot) -> TodoResponse? { + makeResponse(documentID: snapshot.documentID, data: snapshot.data()) + } + + func makeResponse(from snapshot: DocumentSnapshot) -> TodoResponse? { + guard let data = snapshot.data() else { + return nil + } + return makeResponse(documentID: snapshot.documentID, data: data) + } + + func makeResponse(documentID: String, data: [String: Any]) -> TodoResponse? { + guard + let isPinned = data[TodoFieldKey.isPinned.rawValue] as? Bool, + let isCompleted = data[TodoFieldKey.isCompleted.rawValue] as? Bool, + let isChecked = data[TodoFieldKey.isChecked.rawValue] as? Bool, + let title = data[TodoFieldKey.title.rawValue] as? String, + let content = data[TodoFieldKey.content.rawValue] as? String, + let createdAtTimestamp = data[TodoFieldKey.createdAt.rawValue] as? Timestamp, + let updatedAtTimestamp = data[TodoFieldKey.updatedAt.rawValue] as? Timestamp, + let tags = data[TodoFieldKey.tags.rawValue] as? [String], + let kind = data[TodoFieldKey.kind.rawValue] as? String else { + return nil + } + + let dueDate = (data[TodoFieldKey.dueDate.rawValue] as? Timestamp)?.dateValue() + return TodoResponse( + id: documentID, + isPinned: isPinned, + isCompleted: isCompleted, + isChecked: isChecked, + title: title, + content: content, + createdAt: createdAtTimestamp.dateValue(), + updatedAt: updatedAtTimestamp.dateValue(), + dueDate: dueDate, + tags: tags, + kind: kind + ) + } + + enum TodoFieldKey: String { + case id + case isPinned + case isCompleted + case isChecked + case title + case content + case createdAt + case updatedAt + case dueDate + case tags + case kind + } +} diff --git a/DevLog/Infra/Service/WebPageService.swift b/DevLog/Infra/Service/WebPageService.swift index 42db6a34..f2b05c97 100644 --- a/DevLog/Infra/Service/WebPageService.swift +++ b/DevLog/Infra/Service/WebPageService.swift @@ -10,6 +10,7 @@ import FirebaseFirestore final class WebPageService { private let store = Firestore.firestore() + private let encoder = Firestore.Encoder() private let logger = Logger(category: "WebPageService") /// 저장한 웹페이지를 모두 불러옴 @@ -24,9 +25,7 @@ final class WebPageService { do { let collectionRef = store.collection("users/\(uid)/webPages") let snapshot = try await collectionRef.getDocuments() - let items: [WebPageResponse] = snapshot.documents.compactMap { doc in - try? doc.data(as: WebPageResponse.self) - } + let items: [WebPageResponse] = snapshot.documents.compactMap { makeResponse(from: $0) } let trimmedQuery = query.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmedQuery.isEmpty else { @@ -58,7 +57,7 @@ final class WebPageService { do { let documentID = documentID(for: request.url) let docRef = store.document("users/\(uid)/webPages/\(documentID)") - let data = try Firestore.Encoder().encode(request) + let data = try encoder.encode(request) try await docRef.setData(data, merge: true) logger.info("Successfully upserted web page") } catch { @@ -98,3 +97,31 @@ final class WebPageService { .replacingOccurrences(of: "=", with: "") } } + +private extension WebPageService { + func makeResponse(from snapshot: QueryDocumentSnapshot) -> WebPageResponse? { + let data = snapshot.data() + guard + let title = data[WebPageFieldKey.title.rawValue] as? String, + let url = data[WebPageFieldKey.url.rawValue] as? String, + let displayURL = data[WebPageFieldKey.displayURL.rawValue] as? String, + let imageURL = data[WebPageFieldKey.imageURL.rawValue] as? String else { + return nil + } + + return WebPageResponse( + id: snapshot.documentID, + title: title, + url: url, + displayURL: displayURL, + imageURL: imageURL + ) + } + + enum WebPageFieldKey: String { + case title + case url + case displayURL + case imageURL + } +}