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
8 changes: 8 additions & 0 deletions DevLog/App/Assembler/DomainAssembler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,13 @@ final class DomainAssembler: Assembler {
container.register(DeleteWebPageUseCase.self) {
DeleteWebPageUseCaseImpl(container.resolve(WebPageRepository.self))
}

container.register(DeletePushNotificationUseCase.self) {
DeletePushNotificationUseCaseImpl(container.resolve(PushNotificationRepository.self))
}

container.register(FetchPushNotificationsUseCase.self) {
FetchPushNotificationsUseCaseImpl(container.resolve(PushNotificationRepository.self))
}
}
}
25 changes: 25 additions & 0 deletions DevLog/Data/Repository/PushNotificationRepositoryImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,42 @@ final class PushNotificationRepositoryImpl: PushNotificationRepository {
self.service = pushNotificationService
}

/// 푸시 알림 On/Off 설정
func fetchPushNotificationEnabled() async throws -> Bool {
return try await service.fetchPushNotificationEnabled()
}

/// 푸시 알림 시간 설정
func fetchPushNotificationTime() async throws -> DateComponents {
return try await service.fetchPushNotificationTime()
}

/// 푸시 알림 설정 업데이트
func updatePushNotificationSettings(_ settings: PushNotificationSettings) async throws {
try await service.updatePushNotificationSettings(
isEnabled: settings.isEnabled, components: settings.scheduledTime
)
}

/// 푸시 알림 기록 요청
func requestNotifications() async throws -> [PushNotification] {
try await service.requestNotifications()
.compactMap { dto in
dto.id.map { id in
PushNotification(
id: id,
title: dto.title,
body: dto.body,
receivedAt: dto.receivedAt.dateValue(),
isRead: dto.isRead,
todoID: dto.todoID
)
}
}
}

// 푸시 알림 기록 삭제
func deleteNotification(_ notificationID: String) async throws {
try await service.deleteNotification(notificationID)
}
}
2 changes: 2 additions & 0 deletions DevLog/Domain/Protocol/PushNotificationRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ protocol PushNotificationRepository {
func fetchPushNotificationEnabled() async throws -> Bool
func fetchPushNotificationTime() async throws -> DateComponents
func updatePushNotificationSettings(_ settings: PushNotificationSettings) async throws
func requestNotifications() async throws -> [PushNotification]
func deleteNotification(_ notificationID: String) async throws
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//
// DeletePushNotificationUseCase.swift
// DevLog
//
// Created by 최윤진 on 2/10/26.
//

protocol DeletePushNotificationUseCase {
func execute(_ notificationID: String) async throws
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// DeletePushNotificationUseCaseImpl.swift
// DevLog
//
// Created by 최윤진 on 2/10/26.
//

final class DeletePushNotificationUseCaseImpl: DeletePushNotificationUseCase {
private let repository: PushNotificationRepository

init(_ repository: PushNotificationRepository) {
self.repository = repository
}

func execute(_ notificationID: String) async throws {
try await repository.deleteNotification(notificationID)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//
// FetchPushNotificationsUseCase.swift
// DevLog
//
// Created by 최윤진 on 2/10/26.
//

protocol FetchPushNotificationsUseCase {
func execute() async throws -> [PushNotification]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// FetchPushNotificationsUseCaseImpl.swift
// DevLog
//
// Created by 최윤진 on 2/10/26.
//

final class FetchPushNotificationsUseCaseImpl: FetchPushNotificationsUseCase {
private let repository: PushNotificationRepository

init(_ repository: PushNotificationRepository) {
self.repository = repository
}

func execute() async throws -> [PushNotification] {
try await repository.requestNotifications()
}
}
43 changes: 7 additions & 36 deletions DevLog/Infra/DTO/PushNotification.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,12 @@
//

import Foundation
import FirebaseFirestore

struct PushNotification: Codable, Identifiable {
@DocumentID var id: String?
let title: String // 알림 제목
let body: String // 알림 내용
let receivedDate: Date // 알림 수신 날짜
var isRead: Bool // 알림 읽음 여부
let todoId: String // Todo ID

init(from: QueryDocumentSnapshot) {
self.id = from.documentID
self.title = from["title"] as? String ?? ""
self.body = from["body"] as? String ?? ""
self.receivedDate = (from["receivedDate"] as? Timestamp)?.dateValue() ?? Date()
self.isRead = from["isRead"] as? Bool ?? false
self.todoId = from["todoId"] as? String ?? ""
}

init(id: String? = nil, title: String, body: String, receivedDate: Date, isRead: Bool, todoId: String) {
self.id = id
self.title = title
self.body = body
self.receivedDate = receivedDate
self.isRead = isRead
self.todoId = todoId
}

func toDictionary() -> [String: Any] {
return [
"title": title,
"body": body,
"receivedDate": Timestamp(date: receivedDate),
"isRead": isRead,
"todoId": todoId
]
}
struct PushNotification: Identifiable {
let id: String
let title: String
let body: String
let receivedAt: Date
var isRead: Bool
let todoID: String
}
17 changes: 17 additions & 0 deletions DevLog/Infra/DTO/PushNotificationResponse.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// PushNotificationResponse.swift
// DevLog
//
// Created by 최윤진 on 2/10/26.
//

import FirebaseFirestore

struct PushNotificationResponse: Decodable {
@DocumentID var id: String?
let title: String
let body: String
let receivedAt: Timestamp
let isRead: Bool
let todoID: String
}
13 changes: 7 additions & 6 deletions DevLog/Infra/Service/PushNotificationService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,19 @@ final class PushNotificationService {
try await settingsRef.setData(dict, merge: true)
}

/// 푸시 알림 데이터 요청
func requestNotification() async throws -> [PushNotification] {
/// 푸시 알림 기록 요청
func requestNotifications() async throws -> [PushNotificationResponse] {
guard let uid = Auth.auth().currentUser?.uid else { throw AuthError.notAuthenticated }

let collection = store.collection("users/\(uid)/notifications")

let snapshot = try await collection.getDocuments()

return snapshot.documents.compactMap { PushNotification(from: $0) }

return try snapshot.documents.compactMap { document in
try document.data(as: PushNotificationResponse.self)
}
}

/// 푸시 알림 데이터 삭제
/// 푸시 알림 기록 삭제
func deleteNotification(_ notificationID: String) async throws {
guard let uid = Auth.auth().currentUser?.uid else { throw AuthError.notAuthenticated }

Expand Down
123 changes: 120 additions & 3 deletions DevLog/Presentation/ViewModel/PushNotificationViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,141 @@ import Foundation

final class PushNotificationViewModel: Store {
struct State {

var notifications: [PushNotification] = []
var showAlert: Bool = false
var showToast: Bool = false
var alertTitle: String = ""
var alertType: AlertType?
var alertMessage: String = ""
var toastMessage: String = ""
var toastType: ToastType?
var isLoading: Bool = false
var pendingTask: (PushNotification, Int)?
}

enum Action {

case fetchNotifications
case deleteNotification(PushNotification)
case undoDelete
case confirmDelete
case setAlert(isPresented: Bool, type: AlertType? = nil)
case setToast(isPresented: Bool, type: ToastType? = nil)
case setLoading(Bool)
case setNotifications([PushNotification])
}

enum SideEffect {
case fetch
case delete(PushNotification)
}

enum AlertType {
case error
}

enum ToastType {
case delete
}

@Published private(set) var state: State = .init()
private let fetchUseCase: FetchPushNotificationsUseCase
private let deleteUseCase: DeletePushNotificationUseCase

init(
fetchUseCase: FetchPushNotificationsUseCase,
deleteUseCase: DeletePushNotificationUseCase
) {
self.fetchUseCase = fetchUseCase
self.deleteUseCase = deleteUseCase
}

func reduce(with action: Action) -> [SideEffect] {

var state = self.state

switch action {
case .fetchNotifications:
return [.fetch]
case .deleteNotification(let item):
guard let index = state.notifications.firstIndex(where: { $0.id == item.id }) else {
return []
}
state.pendingTask = (item, index)
state.notifications.remove(at: index)
setToast(&state, isPresented: true, for: .delete)
case .undoDelete:
guard let (item, index) = state.pendingTask else { return [] }
state.notifications.insert(item, at: index)
state.pendingTask = nil
case .confirmDelete:
guard let (item, _ ) = state.pendingTask else {
return []
}
return [.delete(item)]
case .setAlert(let isPresented, let type):
setAlert(isPresented: isPresented, for: type)
return []
case .setToast(let isPresented, let type):
setToast(&state, isPresented: isPresented, for: type)
case .setLoading(let value):
state.isLoading = value
case .setNotifications(let notifications):
state.notifications = notifications
}

self.state = state
return []
}

func run(_ effect: SideEffect) {
switch effect {
case .fetch:
Task {
do {
defer { send(.setLoading(false)) }
send(.setLoading(true))
let notifications = try await fetchUseCase.execute()
send(.setNotifications(notifications))
} catch {
send(.setAlert(isPresented: true, type: .error))
}
}
case .delete(let notification):
Task {
do {
try await deleteUseCase.execute(notification.id)
} catch {
send(.setAlert(isPresented: true, type: .error))
}
}
}
}
}

private extension PushNotificationViewModel {
func setAlert(isPresented: Bool, for type: AlertType?) {
switch type {
case .error:
state.alertTitle = "오류"
state.alertMessage = "문제가 발생했습니다. 잠시 후 다시 시도해주세요."
case .none:
state.alertTitle = ""
state.alertMessage = ""
}
state.alertType = type
state.showAlert = isPresented
}

func setToast(
_ state: inout State,
isPresented: Bool,
for type: ToastType?
) {
switch type {
case .delete:
state.toastMessage = "실행 취소"
case .none:
state.toastMessage = ""
}
state.showToast = isPresented
}
}
3 changes: 1 addition & 2 deletions DevLog/Presentation/ViewModel/SearchViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,6 @@ final class SearchViewModel: Store {
do {
defer { send(.setLoading(false)) }
send(.setLoading(true))

try await deleteWebPageUseCase.execute(item.url.absoluteString)
send(.deleteWebPage(item: item, fromEffect: true))
} catch {
Expand All @@ -153,7 +152,7 @@ private extension SearchViewModel {
state.alertMessage = ""
case .error:
state.alertTitle = "오류"
state.alertMessage = "문제가 발생했습니다. 다시 시도해주세요."
state.alertMessage = "문제가 발생했습니다. 잠시 다시 시도해주세요."
case .none:
state.alertTitle = ""
state.alertMessage = ""
Expand Down
6 changes: 6 additions & 0 deletions DevLog/Resource/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,9 @@
},
"미리보기" : {

},
"받은 푸시 알람" : {

},
"버전 정보" : {

Expand Down Expand Up @@ -328,6 +331,9 @@
},
"작성된 내용이 없습니다." : {

},
"작성된 알림이 없습니다." : {

},
"저장된 웹페이지가 없습니다.\n우측 '+' 버튼을 눌러 웹페이지를 추가해보세요." : {

Expand Down
Loading