diff --git a/KkuMulKum/Application/AppDelegate.swift b/KkuMulKum/Application/AppDelegate.swift index 9f9e5431..1cae8b89 100644 --- a/KkuMulKum/Application/AppDelegate.swift +++ b/KkuMulKum/Application/AppDelegate.swift @@ -19,6 +19,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { + + UNUserNotificationCenter.current().delegate = LocalNotificationManager.shared + // KakaoSDK 초기화 과정에서 앱 키를 동적으로 불러오기 if let kakaoAppKey = fetchKakaoAppKeyFromPrivacyInfo() { KakaoSDK.initSDK(appKey: kakaoAppKey) diff --git a/KkuMulKum/Resource/Notification/LocalNotificationManager.swift b/KkuMulKum/Resource/Notification/LocalNotificationManager.swift index b9a7b008..5fff6f38 100644 --- a/KkuMulKum/Resource/Notification/LocalNotificationManager.swift +++ b/KkuMulKum/Resource/Notification/LocalNotificationManager.swift @@ -9,31 +9,67 @@ import Foundation import UserNotifications -class LocalNotificationManager { - +class LocalNotificationManager: NSObject, UNUserNotificationCenterDelegate { + static let shared = LocalNotificationManager() private let notificationCenter = UNUserNotificationCenter.current() - + + private override init() { + super.init() + notificationCenter.delegate = self + } + func requestAuthorization(completion: @escaping (Bool) -> Void) { notificationCenter.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in - completion(granted) + if let error = error { + print("Authorization request error: \(error)") + } + DispatchQueue.main.async { + completion(granted) + } } } - func scheduleNotification(title: String, body: String, triggerDate: Date) { + func scheduleNotification(title: String, body: String, triggerDate: Date, identifier: String, + completion: @escaping ( + Error? + ) -> Void + ) { let content = UNMutableNotificationContent() content.title = title content.body = body content.sound = .default - let components = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: triggerDate) + let components = Calendar.current.dateComponents( + [.year, .month,.day, .hour, .minute, .second ], + from: triggerDate + ) let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: false) - let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger) + let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger) notificationCenter.add(request) { error in - if let error = error { - print("Error scheduling notification: \(error)") + DispatchQueue.main.async { + completion(error) } } } + + func getPendingNotifications(completion: @escaping ([UNNotificationRequest]) -> Void) { + notificationCenter.getPendingNotificationRequests { requests in + DispatchQueue.main.async { + completion(requests) + } + } + } + + func removeAllPendingNotifications() { + notificationCenter.removeAllPendingNotificationRequests() + } + + func userNotificationCenter(_ center: UNUserNotificationCenter, + willPresent notification: UNNotification, + withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void + ) { + completionHandler([.banner, .sound, .badge]) + } } diff --git a/KkuMulKum/Source/Promise/ReadyStatus/ViewController/SetReadyInfoViewController.swift b/KkuMulKum/Source/Promise/ReadyStatus/ViewController/SetReadyInfoViewController.swift index 3ba7065e..7109cc60 100644 --- a/KkuMulKum/Source/Promise/ReadyStatus/ViewController/SetReadyInfoViewController.swift +++ b/KkuMulKum/Source/Promise/ReadyStatus/ViewController/SetReadyInfoViewController.swift @@ -27,6 +27,7 @@ final class SetReadyInfoViewController: BaseViewController { fatalError("init(coder:) has not been implemented") } + // MARK: - LifeCycle override func viewWillAppear(_ animated: Bool) { @@ -47,6 +48,7 @@ final class SetReadyInfoViewController: BaseViewController { setupNavigationBarTitle(with: "준비 정보 입력하기") bindViewModel() + setupTapGesture() } override func setupDelegate() { @@ -95,6 +97,18 @@ final class SetReadyInfoViewController: BaseViewController { private func doneButtonDidTap(_ sender: UIButton) { viewModel.updateReadyInfo() } + + + // MARK: - Keyboard Dismissal + + private func setupTapGesture() { + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) + view.addGestureRecognizer(tapGesture) + } + + @objc private func dismissKeyboard() { + view.endEditing(true) + } } @@ -148,10 +162,9 @@ private extension SetReadyInfoViewController { Toast().show(message: message, view: view, position: .bottom, inset: bottomInset) } - // MARK: - Data Bind - func bindViewModel() { + func bindViewModel() { viewModel.readyHour.bind { [weak self] readyHour in self?.rootView.readyHourTextField.text = readyHour } diff --git a/KkuMulKum/Source/Promise/ReadyStatus/ViewModel/SetReadyInfoViewModel.swift b/KkuMulKum/Source/Promise/ReadyStatus/ViewModel/SetReadyInfoViewModel.swift index 9d5d4ecb..10dd03de 100644 --- a/KkuMulKum/Source/Promise/ReadyStatus/ViewModel/SetReadyInfoViewModel.swift +++ b/KkuMulKum/Source/Promise/ReadyStatus/ViewModel/SetReadyInfoViewModel.swift @@ -6,6 +6,7 @@ // import Foundation +import UserNotifications final class SetReadyInfoViewModel { let promiseID: Int @@ -32,7 +33,7 @@ final class SetReadyInfoViewModel { promiseTime: String, promiseName: String, service: SetReadyStatusInfoServiceType, - notificationManager: LocalNotificationManager = LocalNotificationManager() + notificationManager: LocalNotificationManager = LocalNotificationManager.shared ) { self.promiseID = promiseID self.promiseName = promiseName @@ -79,8 +80,7 @@ final class SetReadyInfoViewModel { calculateTimes() } - func checkValid( - readyHourText: String, + func checkValid(readyHourText: String, readyMinuteText: String, moveHourText: String, moveMinuteText: String @@ -122,25 +122,78 @@ final class SetReadyInfoViewModel { private func scheduleLocalNotification() { let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd HH:mm" - - guard let promiseDate = dateFormatter.date(from: promiseTime) else { - print("Invalid date format") + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + dateFormatter.locale = Locale(identifier: "ko_KR") + dateFormatter.timeZone = TimeZone(identifier: "Asia/Seoul") + + guard let promiseDate = dateFormatter.date(from: self.promiseTime) else { + print("Invalid date format: \(self.promiseTime)") return } - let totalPrepTime = TimeInterval((readyTime + moveTime) * 60) - let notificationDate = promiseDate.addingTimeInterval(-totalPrepTime) + let totalPrepTime = TimeInterval((self.readyTime + self.moveTime) * 60) + + let timeFormatter = DateFormatter() + timeFormatter.dateFormat = "HH:mm:ss" + timeFormatter.timeZone = TimeZone(identifier: "Asia/Seoul") + + print("약속 시간: \(timeFormatter.string(from: promiseDate))") + print("준비 시간: \(self.readyTime) 분") + print("이동 시간: \(self.moveTime) 분") + print("총 준비 시간: \(totalPrepTime / 60) 분") + + let readyStartTime = promiseDate.addingTimeInterval(-TimeInterval(self.readyTime + self.moveTime) * 60) + let moveStartTime = promiseDate.addingTimeInterval(-TimeInterval(self.moveTime) * 60) + + print("준비 시작 시간: \(timeFormatter.string(from: readyStartTime))") + print("이동 시작 시간: \(timeFormatter.string(from: moveStartTime))") - notificationManager.requestAuthorization { granted in + self.notificationManager.requestAuthorization { [weak self] granted in + guard let self = self else { return } if granted { + UNUserNotificationCenter.current().getNotificationSettings { settings in + print("현재 알림 설정: \(settings)") + } + + self.notificationManager.removeAllPendingNotifications() + self.notificationManager.scheduleNotification( title: "준비 시작", body: "\(self.promiseName) 약속 준비를 시작할 시간입니다!", - triggerDate: notificationDate - ) + triggerDate: readyStartTime, + identifier: "readyStart_\(self.promiseID)" + ) { error in + if let error = error { + print("준비 시작 알림 설정 실패: \(error)") + } else { + print("준비 시작 알림이 \(timeFormatter.string(from: readyStartTime))에 설정되었습니다.") + } + } + + self.notificationManager.scheduleNotification( + title: "이동 시작", + body: "\(self.promiseName) 약속 장소로 이동할 시간입니다!", + triggerDate: moveStartTime, + identifier: "moveStart_\(self.promiseID)" + ) { error in + if let error = error { + print("이동 시작 알림 설정 실패: \(error)") + } else { + print("이동 시작 알림이 \(timeFormatter.string(from: moveStartTime))에 설정되었습니다.") + } + } + + self.notificationManager.getPendingNotifications { requests in + print("예정된 알림 수: \(requests.count)") + for request in requests { + if let trigger = request.trigger as? UNCalendarNotificationTrigger, + let nextTriggerDate = trigger.nextTriggerDate() { + print("알림 ID: \(request.identifier), 예정 시간: \(timeFormatter.string(from: nextTriggerDate))") + } + } + } } else { - print("Notification permission not granted") + print("알림 권한이 허용되지 않았습니다.") } } }