Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions DevLog/Data/DTO/TodoCursorDTO.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// TodoCursorDTO.swift
// DevLog
//
// Created by opfic on 2/21/26.
//

import Foundation

struct TodoCursorDTO {
let createdAt: Date
let documentID: String
}
37 changes: 37 additions & 0 deletions DevLog/Data/DTO/TodoDTO.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// TodoDTO.swift
// DevLog
//
// Created by 최윤진 on 12/14/25.
//

import Foundation

struct TodoRequest: Dictionaryable {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

TodoRequestDictionaryable 프로토콜을 채택하고 있는데, Dictionaryable의 구현이 FirebaseFirestore에 의존하고 있습니다. 이 PR의 목표가 데이터 레이어와 인프라 레이어를 분리하는 것인 만큼, 데이터 레이어의 DTO가 특정 인프라(Firebase) 기술에 의존하지 않도록 하는 것이 중요합니다.

Dictionaryable 프로토콜 채택을 제거하고, TodoRequest를 딕셔너리로 변환하는 로직은 Infra 레이어(예: TodoService) 내부에서 처리하는 것을 제안합니다. 이렇게 하면 데이터 레이어의 독립성을 유지할 수 있습니다.

예를 들어, TodoService.swift에 다음과 같은 private extension을 추가할 수 있습니다.

// In TodoService.swift
private extension TodoRequest {
    func toDictionary() -> [String: Any] {
        // Firestore.Encoder를 사용한 직렬화 로직
    }
}
Suggested change
struct TodoRequest: Dictionaryable {
struct TodoRequest {

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

}
Comment on lines +10 to +23
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

TodoRequest를 인코딩할 때 id 프로퍼티가 인코딩에서 제외되도록 CodingKeys를 명시적으로 정의하는 것을 고려해 보세요. id는 Firestore 문서의 ID로 사용되며, 데이터 페이로드에는 포함되지 않는 것이 일반적입니다. CodingKeys를 사용하면 TodoService에서 수동으로 id 키를 제거하는 로직을 없앨 수 있어, DTO의 역할이 더 명확해지고 코드가 간결해집니다.

이 변경을 적용하려면 TodoServiceupsertTodo 메서드에서 data.removeValue(forKey: TodoFieldKey.id.rawValue) 라인도 함께 제거해야 합니다.

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

    enum CodingKeys: String, CodingKey {
        case isPinned, isCompleted, isChecked, title, content, createdAt, updatedAt, dueDate, tags, kind
    }
}


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
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@

struct TodoPageResponse {
let items: [TodoResponse]
let nextCursor: TodoCursorResponse?
let nextCursor: TodoCursorDTO?
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// Created by 최윤진 on 2/9/26.
//

import FirebaseFirestore
import Foundation

struct WebPageRequest: Encodable {
let title: String
Expand All @@ -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
Expand Down
11 changes: 3 additions & 8 deletions DevLog/Data/Mapper/PushNotificationMapping.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
}
Expand All @@ -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
Expand All @@ -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
)
}
Expand Down
13 changes: 4 additions & 9 deletions DevLog/Data/Mapper/TodoMapping.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
// Created by 최윤진 on 2/19/26.
//

import FirebaseFirestore

extension TodoRequest {
static func fromDomain(_ entity: Todo) -> Self {
TodoRequest(
Expand All @@ -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)")
}
Expand All @@ -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
)
}
Expand Down
2 changes: 1 addition & 1 deletion DevLog/Data/Repository/TodoRepositoryImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
13 changes: 0 additions & 13 deletions DevLog/Infra/DTO/TodoCursorResponse.swift

This file was deleted.

79 changes: 0 additions & 79 deletions DevLog/Infra/DTO/TodoDTO.swift

This file was deleted.

2 changes: 1 addition & 1 deletion DevLog/Infra/Extension/FirebaseAuthUser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Foundation
import FirebaseAuth

extension FirebaseAuth.User {
func toResponse(
func makeResponse(
providerID: AuthProviderID,
fcmToken: String,
accessToken: String? = nil
Expand Down
33 changes: 28 additions & 5 deletions DevLog/Infra/Service/PushNotificationService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ final class PushNotificationService {

if let cursor {
firestoreQuery = firestoreQuery.start(after: [
cursor.receivedAt,
Timestamp(date: cursor.receivedAt),
cursor.documentID
])
}
Expand All @@ -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 {
return nil
}

return PushNotificationCursorDTO(
receivedAt: receivedAt,
receivedAt: receivedAt.dateValue(),
documentID: document.documentID
)
} ?? nil
Expand Down Expand Up @@ -177,3 +175,28 @@ 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["title"] as? String,
let body = data["body"] as? String,
let receivedAt = data["receivedAt"] as? Timestamp,
let isRead = data["isRead"] as? Bool,
let todoID = data["todoID"] as? String,
let todoKind = data["todoKind"] as? String else {
return nil
}

return PushNotificationResponse(
id: snapshot.documentID,
title: title,
body: body,
receivedAt: receivedAt.dateValue(),
isRead: isRead,
todoID: todoID,
todoKind: todoKind
)
}
}
Comment on lines +179 to +211
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Firestore 필드 이름을 수동으로 파싱하는 로직을 추가하셨네요. 문자열 리터럴을 직접 사용하는 것은 오타에 취약하여 런타임 에러를 유발할 수 있습니다. 필드 이름을 enum으로 정의하여 상수로 관리하면 타입-세이프하게 접근할 수 있어 코드의 안정성과 유지보수성을 높일 수 있습니다.

private extension PushNotificationService {
    private enum FieldKey: String {
        case title, body, receivedAt, isRead, todoID, todoKind
    }

    func makeResponse(from snapshot: QueryDocumentSnapshot) -> PushNotificationResponse? {
        let data = snapshot.data()
        guard
            let title = data[FieldKey.title.rawValue] as? String,
            let body = data[FieldKey.body.rawValue] as? String,
            let receivedAt = data[FieldKey.receivedAt.rawValue] as? Timestamp,
            let isRead = data[FieldKey.isRead.rawValue] as? Bool,
            let todoID = data[FieldKey.todoID.rawValue] as? String,
            let todoKind = data[FieldKey.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
        )
    }
}

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading