diff --git a/FiveGuyes/FiveGuyes.xcodeproj/project.pbxproj b/FiveGuyes/FiveGuyes.xcodeproj/project.pbxproj index 0424302..2f7de98 100644 --- a/FiveGuyes/FiveGuyes.xcodeproj/project.pbxproj +++ b/FiveGuyes/FiveGuyes.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ 1A27D9662CDB779000D1E14D /* TotalCalendarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A27D9652CDB779000D1E14D /* TotalCalendarView.swift */; }; 1A54142B2CD9FEF400283FBD /* BookSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A54142A2CD9FEF400283FBD /* BookSearchView.swift */; }; 1AA14A782CDDA30D00B763A6 /* TotalCalendarTextBubble.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AA14A772CDDA30D00B763A6 /* TotalCalendarTextBubble.swift */; }; + 1AA14A7A2CDDD96200B763A6 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AA14A792CDDD96200B763A6 /* NotificationManager.swift */; }; 24360D482CD8BAF100E83D2B /* EmptyNotiView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24360D472CD8BAF100E83D2B /* EmptyNotiView.swift */; }; 24360D4A2CD8BE3C00E83D2B /* FinishGoalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24360D492CD8BE3C00E83D2B /* FinishGoalView.swift */; }; 24360D532CD9F3AF00E83D2B /* DailyProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24360D522CD9F3AF00E83D2B /* DailyProgressView.swift */; }; @@ -39,6 +40,7 @@ 26EBDE262CDE3C6000B3A2BC /* BookSettingInputModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26EBDE252CDE3C6000B3A2BC /* BookSettingInputModel.swift */; }; 26EBDE2A2CDF33D900B3A2BC /* Date+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26EBDE292CDF33D900B3A2BC /* Date+Extension.swift */; }; 26EBDE2C2CE1341800B3A2BC /* UserBook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26EBDE2B2CE1341800B3A2BC /* UserBook.swift */; }; + 26EBDE2F2CE3856900B3A2BC /* NotificationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26EBDE2E2CE3856900B3A2BC /* NotificationType.swift */; }; 26F19A682CD9EFD800F41D6D /* MainHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26F19A672CD9EFD800F41D6D /* MainHomeView.swift */; }; 26F19A6A2CD9FE5C00F41D6D /* WeeklyReadingProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26F19A692CD9FE5C00F41D6D /* WeeklyReadingProgressView.swift */; }; 26F19A6C2CDA0A2B00F41D6D /* CompletionListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26F19A6B2CDA0A2B00F41D6D /* CompletionListView.swift */; }; @@ -47,7 +49,6 @@ 26F19A742CDB271E00F41D6D /* NavigationRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26F19A732CDB271E00F41D6D /* NavigationRootView.swift */; }; 26F19A782CDB5D0100F41D6D /* BookDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26F19A772CDB5D0100F41D6D /* BookDetails.swift */; }; 26F19A7A2CDB5F3900F41D6D /* ReadingScheduleCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26F19A792CDB5F3900F41D6D /* ReadingScheduleCalculator.swift */; }; - 26F19A822CDBE3A600F41D6D /* ReadingTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26F19A812CDBE3A600F41D6D /* ReadingTestView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -63,6 +64,8 @@ 1A27D9652CDB779000D1E14D /* TotalCalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TotalCalendarView.swift; sourceTree = ""; }; 1A54142A2CD9FEF400283FBD /* BookSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BookSearchView.swift; path = FiveGuyes/Sources/Views/Screen/BookSetting/BookSearch/BookSearchView.swift; sourceTree = SOURCE_ROOT; }; 1AA14A772CDDA30D00B763A6 /* TotalCalendarTextBubble.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TotalCalendarTextBubble.swift; sourceTree = ""; }; + 1AA14A792CDDD96200B763A6 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; + 24360D452CD8A2F800E83D2B /* EmptyDataMainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyDataMainView.swift; sourceTree = ""; }; 24360D472CD8BAF100E83D2B /* EmptyNotiView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyNotiView.swift; sourceTree = ""; }; 24360D492CD8BE3C00E83D2B /* FinishGoalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinishGoalView.swift; sourceTree = ""; }; 24360D522CD9F3AF00E83D2B /* DailyProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DailyProgressView.swift; sourceTree = ""; }; @@ -85,6 +88,7 @@ 26EBDE252CDE3C6000B3A2BC /* BookSettingInputModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookSettingInputModel.swift; sourceTree = ""; }; 26EBDE292CDF33D900B3A2BC /* Date+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extension.swift"; sourceTree = ""; }; 26EBDE2B2CE1341800B3A2BC /* UserBook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserBook.swift; sourceTree = ""; }; + 26EBDE2E2CE3856900B3A2BC /* NotificationType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationType.swift; sourceTree = ""; }; 26F19A672CD9EFD800F41D6D /* MainHomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeView.swift; sourceTree = ""; }; 26F19A692CD9FE5C00F41D6D /* WeeklyReadingProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeeklyReadingProgressView.swift; sourceTree = ""; }; 26F19A6B2CDA0A2B00F41D6D /* CompletionListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletionListView.swift; sourceTree = ""; }; @@ -93,7 +97,6 @@ 26F19A732CDB271E00F41D6D /* NavigationRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRootView.swift; sourceTree = ""; }; 26F19A772CDB5D0100F41D6D /* BookDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookDetails.swift; sourceTree = ""; }; 26F19A792CDB5F3900F41D6D /* ReadingScheduleCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadingScheduleCalculator.swift; sourceTree = ""; }; - 26F19A812CDBE3A600F41D6D /* ReadingTestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadingTestView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -142,6 +145,7 @@ 261714D32CDD27C900A3241D /* Config.xcconfig */, 26890B932CAE811A008DFF49 /* FiveGuyes */, 26890B922CAE811A008DFF49 /* Products */, + 26EBDE2D2CE3307C00B3A2BC /* Recovered References */, ); sourceTree = ""; }; @@ -218,7 +222,6 @@ 26D852FB2CCE40BC0016239A /* Screen */ = { isa = PBXGroup; children = ( - 26F19A832CDC970D00F41D6D /* Testing */, 26F19A662CD9EF9400F41D6D /* Main */, 1A27D95E2CDA155B00D1E14D /* BookSetting */, 24360D4F2CD9F2D800E83D2B /* BookProgress */, @@ -264,6 +267,8 @@ 26F19A772CDB5D0100F41D6D /* BookDetails.swift */, 26EBDE2B2CE1341800B3A2BC /* UserBook.swift */, 26F19A792CDB5F3900F41D6D /* ReadingScheduleCalculator.swift */, + 26EBDE2E2CE3856900B3A2BC /* NotificationType.swift */, + 1AA14A792CDDD96200B763A6 /* NotificationManager.swift */, ); path = Models; sourceTree = ""; @@ -300,20 +305,20 @@ path = BookSearch; sourceTree = ""; }; - 26F19A662CD9EF9400F41D6D /* Main */ = { + 26EBDE2D2CE3307C00B3A2BC /* Recovered References */ = { isa = PBXGroup; children = ( - 26F19A672CD9EFD800F41D6D /* MainHomeView.swift */, + 24360D452CD8A2F800E83D2B /* EmptyDataMainView.swift */, ); - path = Main; + name = "Recovered References"; sourceTree = ""; }; - 26F19A832CDC970D00F41D6D /* Testing */ = { + 26F19A662CD9EF9400F41D6D /* Main */ = { isa = PBXGroup; children = ( - 26F19A812CDBE3A600F41D6D /* ReadingTestView.swift */, + 26F19A672CD9EFD800F41D6D /* MainHomeView.swift */, ); - path = Testing; + path = Main; sourceTree = ""; }; /* End PBXGroup section */ @@ -394,7 +399,6 @@ files = ( 1A54142B2CD9FEF400283FBD /* BookSearchView.swift in Sources */, 26F19A782CDB5D0100F41D6D /* BookDetails.swift in Sources */, - 26F19A822CDBE3A600F41D6D /* ReadingTestView.swift in Sources */, 1A010EAE2CD8A86E00FBE3B3 /* APIStore.swift in Sources */, 24A2063F2CDB180000964FBB /* CompletionCalendarView.swift in Sources */, 1A010EBE2CD8DA2400FBE3B3 /* BookListView.swift in Sources */, @@ -416,6 +420,7 @@ 24360D4A2CD8BE3C00E83D2B /* FinishGoalView.swift in Sources */, 26F19A682CD9EFD800F41D6D /* MainHomeView.swift in Sources */, 26F19A742CDB271E00F41D6D /* NavigationRootView.swift in Sources */, + 1AA14A7A2CDDD96200B763A6 /* NotificationManager.swift in Sources */, 264440502CD8A3AC0031A290 /* CompletionReviewView.swift in Sources */, 264440502CD8A3AC0031A290 /* CompletionReviewView.swift in Sources */, 26890B952CAE811A008DFF49 /* FiveGuyesApp.swift in Sources */, @@ -425,6 +430,7 @@ 264440572CD8E0D20031A290 /* CustomTextEditorStyle.swift in Sources */, 264440552CD8E0100031A290 /* KeyboardObserver.swift in Sources */, 26F19A6C2CDA0A2B00F41D6D /* CompletionListView.swift in Sources */, + 26EBDE2F2CE3856900B3A2BC /* NotificationType.swift in Sources */, 1A27D9662CDB779000D1E14D /* TotalCalendarView.swift in Sources */, 2644404E2CD8A39E0031A290 /* CompletionCelebrationView.swift in Sources */, 26EBDE262CDE3C6000B3A2BC /* BookSettingInputModel.swift in Sources */, diff --git a/FiveGuyes/FiveGuyes/Resources/Assets.xcassets/CircleButtonFilled.imageset/1.png b/FiveGuyes/FiveGuyes/Resources/Assets.xcassets/CircleButtonFilled.imageset/1.png deleted file mode 100644 index 648f68f..0000000 Binary files a/FiveGuyes/FiveGuyes/Resources/Assets.xcassets/CircleButtonFilled.imageset/1.png and /dev/null differ diff --git a/FiveGuyes/FiveGuyes/Resources/Assets.xcassets/CircleButtonFilled.imageset/2.png b/FiveGuyes/FiveGuyes/Resources/Assets.xcassets/CircleButtonFilled.imageset/2.png deleted file mode 100644 index e014a0c..0000000 Binary files a/FiveGuyes/FiveGuyes/Resources/Assets.xcassets/CircleButtonFilled.imageset/2.png and /dev/null differ diff --git a/FiveGuyes/FiveGuyes/Resources/Assets.xcassets/CircleButtonFilled.imageset/3.png b/FiveGuyes/FiveGuyes/Resources/Assets.xcassets/CircleButtonFilled.imageset/3.png deleted file mode 100644 index 5716cd8..0000000 Binary files a/FiveGuyes/FiveGuyes/Resources/Assets.xcassets/CircleButtonFilled.imageset/3.png and /dev/null differ diff --git a/FiveGuyes/FiveGuyes/Resources/Assets.xcassets/CircleButtonFilled.imageset/Contents.json b/FiveGuyes/FiveGuyes/Resources/Assets.xcassets/CircleButtonFilled.imageset/Contents.json deleted file mode 100644 index 511668c..0000000 --- a/FiveGuyes/FiveGuyes/Resources/Assets.xcassets/CircleButtonFilled.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "filename" : "1.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "2.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "3.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/FiveGuyes/FiveGuyes/Sources/Models/NotificationManager.swift b/FiveGuyes/FiveGuyes/Sources/Models/NotificationManager.swift new file mode 100644 index 0000000..5708dfa --- /dev/null +++ b/FiveGuyes/FiveGuyes/Sources/Models/NotificationManager.swift @@ -0,0 +1,88 @@ +// +// NotificationManager.swift +// FiveGuyes +// +// Created by Shim Hyeonhee on 11/8/24. +// + +import UserNotifications + +final class NotificationManager { + private let notificationCenter = UNUserNotificationCenter.current() + private var isGranted: Bool = false + + func setupNotifications(notificationType: NotificationType) async { + await requestAuthorization() + + if isGranted { + await scheduleReminderNotification(notificationType: notificationType) + } + } + + /// 요청한 Noticifation을 모두 지우는 함수 + func clearRequests() { + notificationCenter.removeAllPendingNotificationRequests() + } + + /// Notification 권한 요청 함수 + private func requestAuthorization() async { + do { + try await notificationCenter + .requestAuthorization(options: [.sound, .badge, .alert]) + } catch { + print("❌ NotificationManager/requestAuthorization: \(error.localizedDescription)") + } + + await getCurrentSettings() + } + + /// 현재 Notification 권한 설정을 가져오는 함수 + private func getCurrentSettings() async { + let currentSettings = await notificationCenter.notificationSettings() + + isGranted = (currentSettings.authorizationStatus == .authorized) + } + + private func scheduleReminderNotification(notificationType: NotificationType) async { + // dateContent가 nil일 경우 알림을 보내지 않음 + guard let date = notificationType.dateContent() else { + print("❌ NotificationManager: 다음 읽기 날짜가 없어 알림을 생성하지 않습니다.") + return + } + + let dateComponents = makeDateComponents(date: date, notificationType) + let content = makeNotificationContent(notificationType) + + let identifier = notificationType.identifier() + + // 설정대로 트리거, 요청 셋팅 + let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: false) + let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger) + + do { + try await notificationCenter.add(request) + print("💯 노티 설정 완료") + } catch { + print("❌ NotificationManager/schedule: \(error.localizedDescription)") + } + } + + private func makeDateComponents(date: Date, _ notificationType: NotificationType) -> DateComponents { + let calendar = Calendar.current + let day = calendar.component(.day, from: date) + let month = calendar.component(.month, from: date) + let year = calendar.component(.year, from: date) + let (hour, minute) = notificationType.timeContent() + print("💯노티 설정: \(date) \(hour): \(minute)") + return DateComponents(year: year, month: month, day: day, hour: hour, minute: minute) + } + + private func makeNotificationContent(_ notificationType: NotificationType) -> UNMutableNotificationContent { + let content = UNMutableNotificationContent() + let (title, body) = notificationType.descriptionContent() + content.title = title + content.body = body + + return content + } +} diff --git a/FiveGuyes/FiveGuyes/Sources/Models/NotificationType.swift b/FiveGuyes/FiveGuyes/Sources/Models/NotificationType.swift new file mode 100644 index 0000000..e38ca9b --- /dev/null +++ b/FiveGuyes/FiveGuyes/Sources/Models/NotificationType.swift @@ -0,0 +1,53 @@ +// +// NotificationType.swift +// FiveGuyes +// +// Created by zaehorang on 11/12/24. +// + +import Foundation + +enum NotificationType { + case morning(readingBook: UserBook) + case night(readingBook: UserBook) + + func descriptionContent() -> (title: String, body: String) { + switch self { + case .morning(let readingBook): + let title = "오늘의 한입, 준비됐나요?" + let body = "오늘은 \(readingBook.findNextReadingPagesPerDay())페이지만 읽으면 돼요 멍멍!" + return (title, body) + + case .night: + let title = "오늘의 한입독서를 놓치고 계신가요?" + let body = "오늘 완독하지 않았어요!\n완독이가 물어버릴거에요 🥎 왕왕" + return (title, body) + } + } + + func dateContent() -> Date? { + switch self { + case .morning(let readingBook), .night(let readingBook): + return readingBook.findNextReadingDay() + } + } + + func timeContent() -> (hour: Int, minute: Int) { + switch self { + case .morning: + return (11, 0) + case .night: + return (23, 0) + } + } + + /// 고유 identifier 생성 메서드 + func identifier() -> String { + switch self { + case .morning(let readingBook): + return "\(readingBook.book.title)-morning" + case .night(let readingBook): + return "\(readingBook.book.title)-night" + } + } +} diff --git a/FiveGuyes/FiveGuyes/Sources/Models/ReadingScheduleCalculator.swift b/FiveGuyes/FiveGuyes/Sources/Models/ReadingScheduleCalculator.swift index bb5d789..14af7ed 100644 --- a/FiveGuyes/FiveGuyes/Sources/Models/ReadingScheduleCalculator.swift +++ b/FiveGuyes/FiveGuyes/Sources/Models/ReadingScheduleCalculator.swift @@ -24,8 +24,7 @@ struct ReadingScheduleCalculator { // MARK: 첫날을 기준으로 읽어야하는 페이지를 할당하는 메서드 (초기 페이지 계산) func calculateInitialDailyTargets(for currentReadingBook: UserBook) { - let pagesPerDay = firstCalculatePagesPerDay(for: currentReadingBook) - let remainderPages = firstCalculateRemainderPages(for: currentReadingBook) + let (pagesPerDay, remainderPages) = firstCalculatePagesPerDay(for: currentReadingBook) var targetDate = currentReadingBook.book.startDate var remainderOffset = remainderPages @@ -78,7 +77,7 @@ struct ReadingScheduleCalculator { } } - //MARK: 더 읽거나, 덜 읽으면 이후 날짜의 할당량을 다시 계산한다. + // MARK: 더 읽거나, 덜 읽으면 이후 날짜의 할당량을 다시 계산한다. func adjustFutureTargets(for currentReadingBook: UserBook, from date: Date) { let totalRemainingPages = calculateRemainingPages(for: currentReadingBook) print("❌: \(totalRemainingPages)") @@ -126,16 +125,10 @@ struct ReadingScheduleCalculator { func reassignPagesFromLastReadDate(for currentReadingBook: UserBook) { // 이미 읽었으면 재분배 x if hasReadPagesToday(for: currentReadingBook) { return } - - // 몇 페이지 남음? - let totalRemainingPages = calculateRemainingPages(for: currentReadingBook) - print("❌re: \(totalRemainingPages)") - // 오늘부터 며칠 남음? - let remainingDays = calculateRemainingReadingDays(for: currentReadingBook) - print("🐶re: \(remainingDays)") - // 남은 페이지와 날짜를 기준으로 새롭게 할당량 계산 - let pagesPerDay = totalRemainingPages / remainingDays - var remainderOffset = totalRemainingPages % remainingDays + + // 남은 페이지와 일수를 기준으로 새롭게 할당량 계산 + let (pagesPerDay, remainderPages) = calculatePagesPerDay(for: currentReadingBook) + var remainderOffset = remainderPages var cumulativePages = currentReadingBook.lastPagesRead var targetDate = Date() // 오늘 날짜부터 새로 할당 시작 @@ -187,16 +180,12 @@ struct ReadingScheduleCalculator { } // 하루에 몇 페이지 읽는지 계산 - func firstCalculatePagesPerDay(for currentReadingBook: UserBook) -> Int { - let totalReadingDays = firstCalculateTotalReadingDays(for: currentReadingBook) - return currentReadingBook.book.totalPages / totalReadingDays - } - - - // 하루에 몇 페이지 읽는지 계산하고 딱 떨어지지 않는 페이지 수 구하는 메서드 - func firstCalculateRemainderPages(for currentReadingBook: UserBook) -> Int { + func firstCalculatePagesPerDay(for currentReadingBook: UserBook) -> (pagesPerDay: Int, remainder: Int) { let totalReadingDays = firstCalculateTotalReadingDays(for: currentReadingBook) - return currentReadingBook.book.totalPages % totalReadingDays + let pagesPerDay = currentReadingBook.book.totalPages / totalReadingDays + let remainder = currentReadingBook.book.totalPages % totalReadingDays + + return (pagesPerDay, remainder) } // MARK: - 남은 양을 다시 계산할 때 사용하는 메서드 @@ -220,6 +209,19 @@ struct ReadingScheduleCalculator { return remainingDays } + // 남은 페이지와 날짜를 기반으로 일일 할당량을 계산하는 메서드 + func calculatePagesPerDay(for currentReadingBook: UserBook) -> (pagesPerDay: Int, remainder: Int) { + let totalRemainingPages = calculateRemainingPages(for: currentReadingBook) + let remainingDays = calculateRemainingReadingDays(for: currentReadingBook) + + let pagesPerDay = totalRemainingPages / remainingDays + let remainder = totalRemainingPages % remainingDays + + print("❌읽는 중: \(totalRemainingPages)") + print("🐶읽는 중: \(remainingDays)") + + return (pagesPerDay, remainder) + } // 특정 날의 묙표량과 실제 읽은 페이지의 수를 가져오는 메서드 func getReadingRecord(for currentReadingBook: UserBook, for date: Date) -> ReadingRecord? { @@ -228,4 +230,3 @@ struct ReadingScheduleCalculator { return currentReadingBook.readingRecords[dateKey] } } - diff --git a/FiveGuyes/FiveGuyes/Sources/Models/UserBook.swift b/FiveGuyes/FiveGuyes/Sources/Models/UserBook.swift index 1727296..a7b032f 100644 --- a/FiveGuyes/FiveGuyes/Sources/Models/UserBook.swift +++ b/FiveGuyes/FiveGuyes/Sources/Models/UserBook.swift @@ -30,7 +30,9 @@ final class UserBook { init(book: BookDetails) { self.book = book } - +} + +extension UserBook { func markAsCompleted(review: String) { // 책을 완독 상태로 설정 book.targetEndDate = Date() @@ -53,4 +55,27 @@ final class UserBook { } return readingRecords.values.filter { $0.pagesRead > 0 }.count } + + /// 오늘 이후 다음 읽기 예정일을 반환하는 메서드 + func findNextReadingDay() -> Date? { + let today = lastReadDate ?? Date() + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd" + let todayString = dateFormatter.string(from: today) + print("today⭐️: \(today)") + + // 오늘 이후 날짜들 중 비독서일을 제외한 첫 읽기 예정일을 찾음 + for dateString in readingRecords.keys.sorted() + where dateString > todayString { + return dateFormatter.date(from: dateString) + } + // 모든 읽기 예정일이 지난 경우 nil 반환 + return nil + } + + func findNextReadingPagesPerDay() -> Int { + let readingScheduleCalculator = ReadingScheduleCalculator() + + return readingScheduleCalculator.calculatePagesPerDay(for: self).pagesPerDay + } } diff --git a/FiveGuyes/FiveGuyes/Sources/Stores/APIStore.swift b/FiveGuyes/FiveGuyes/Sources/Stores/APIStore.swift index 1621f1a..3799c86 100644 --- a/FiveGuyes/FiveGuyes/Sources/Stores/APIStore.swift +++ b/FiveGuyes/FiveGuyes/Sources/Stores/APIStore.swift @@ -41,10 +41,7 @@ class APIStore { } let (data, _) = try await URLSession.shared.data(from: url) - print(data) let bookDetailResponse = try JSONDecoder().decode(BookDetailResponse.self, from: data) - print(bookDetailResponse) - print(bookDetailResponse.item?.first?.subInfo?.itemPage) return bookDetailResponse.item?.first?.subInfo?.itemPage ?? 0 } } diff --git a/FiveGuyes/FiveGuyes/Sources/Stores/KeyboardObserver.swift b/FiveGuyes/FiveGuyes/Sources/Stores/KeyboardObserver.swift index e9b4ca5..746112a 100644 --- a/FiveGuyes/FiveGuyes/Sources/Stores/KeyboardObserver.swift +++ b/FiveGuyes/FiveGuyes/Sources/Stores/KeyboardObserver.swift @@ -7,7 +7,7 @@ import UIKit -//MARK: - 키보드가 올라오는 시점을 알기 위한 모델 +// MARK: - 키보드가 올라오는 시점을 알기 위한 모델 final class KeyboardObserver: ObservableObject { @Published var keyboardIsVisible: Bool = false diff --git a/FiveGuyes/FiveGuyes/Sources/Views/Components/WeeklyPageCalendarView.swift b/FiveGuyes/FiveGuyes/Sources/Views/Components/WeeklyPageCalendarView.swift index b21fa5a..8d7f302 100644 --- a/FiveGuyes/FiveGuyes/Sources/Views/Components/WeeklyPageCalendarView.swift +++ b/FiveGuyes/FiveGuyes/Sources/Views/Components/WeeklyPageCalendarView.swift @@ -99,6 +99,11 @@ struct WeeklyPageCalendarView: View { .frame(height: 44) } } else { + if index == todayIndex { // today + Circle() + .fill(Color(red: 0.07, green: 0.87, blue: 0.54)) + .frame(height: 44) + } Text("") .frame(height: 44) .foregroundColor(Color(red: 0.44, green: 0.44, blue: 0.44)) diff --git a/FiveGuyes/FiveGuyes/Sources/Views/Screen/BookCompletion/CompletionCelebrationView.swift b/FiveGuyes/FiveGuyes/Sources/Views/Screen/BookCompletion/CompletionCelebrationView.swift index f53b576..bb52dca 100644 --- a/FiveGuyes/FiveGuyes/Sources/Views/Screen/BookCompletion/CompletionCelebrationView.swift +++ b/FiveGuyes/FiveGuyes/Sources/Views/Screen/BookCompletion/CompletionCelebrationView.swift @@ -105,7 +105,7 @@ struct CompletionCelebrationView: View { let startDateText = book.startDate.toKoreanDateString() // TODO: 완독을 수정할 수도 있기 때문에 완독 날짜가 바뀔 수 있음, 그래서 완독 날짜는 최종에서 업데이트하고 여기서는 오늘 날짜로 보여주기 let endDateText = Date().toKoreanDateString() - let pagesPerDay = readingScheduleCalculator.firstCalculatePagesPerDay(for: userBook) + let pagesPerDay = readingScheduleCalculator.firstCalculatePagesPerDay(for: userBook).pagesPerDay let totalReadingDays = readingScheduleCalculator.firstCalculateTotalReadingDays(for: userBook) return Text("\(startDateText)부터 \(endDateText)까지\n꾸준히 \(pagesPerDay)쪽씩 \(totalReadingDays)일동안 읽었어요 🎉") diff --git a/FiveGuyes/FiveGuyes/Sources/Views/Screen/BookProgress/DailyProgressView.swift b/FiveGuyes/FiveGuyes/Sources/Views/Screen/BookProgress/DailyProgressView.swift index 4e18922..bfaf9e6 100644 --- a/FiveGuyes/FiveGuyes/Sources/Views/Screen/BookProgress/DailyProgressView.swift +++ b/FiveGuyes/FiveGuyes/Sources/Views/Screen/BookProgress/DailyProgressView.swift @@ -17,8 +17,10 @@ struct DailyProgressView: View { @Query(filter: #Predicate { $0.isCompleted == false }) private var currentlyReadingBooks: [UserBook] // 현재 읽고 있는 책을 가져오는 쿼리 - let alertText = "전체쪽수를 초과해서 작성했어요!" - let alertMessage = "끝까지 읽은 게 맞나요?" + private let alertText = "전체쪽수를 초과해서 작성했어요!" + private let alertMessage = "끝까지 읽은 게 맞나요?" + + private let notificationManager = NotificationManager() private var today: Date { // TODO: today가 전날로 나와서 일단 하루 더함 @@ -78,11 +80,18 @@ struct DailyProgressView: View { book.targetEndDate = book.targetEndDate.addDays(1) readingScheduleCalculator.updateReadingProgress(for: userBook, pagesRead: pagesToReadToday, from: today) + + // 노티 세팅하기 + setNotification(userBook) + navigationCoordinator.popToRoot() } else { // 오늘 할당량 기록 readingScheduleCalculator.updateReadingProgress(for: userBook, pagesRead: pagesToReadToday, from: today) + // 노티 세팅하기 + setNotification(userBook) + if pagesToReadToday != book.totalPages { navigationCoordinator.popToRoot() } else { @@ -135,4 +144,13 @@ struct DailyProgressView: View { isTextTextFieldFocused = true } } + + private func setNotification(_ readingBook: UserBook) { + notificationManager.clearRequests() + Task { + await self.notificationManager.setupNotifications(notificationType: .morning(readingBook: readingBook)) + + await self.notificationManager.setupNotifications(notificationType: .night(readingBook: readingBook)) + } + } } diff --git a/FiveGuyes/FiveGuyes/Sources/Views/Screen/BookSetting/FinishGoalView.swift b/FiveGuyes/FiveGuyes/Sources/Views/Screen/BookSetting/FinishGoalView.swift index 7be68fb..90b1d01 100644 --- a/FiveGuyes/FiveGuyes/Sources/Views/Screen/BookSetting/FinishGoalView.swift +++ b/FiveGuyes/FiveGuyes/Sources/Views/Screen/BookSetting/FinishGoalView.swift @@ -15,7 +15,8 @@ struct FinishGoalView: View { @State private var pagesPerDay: Int = 0 @State var userBook: UserBook? - let calculator = ReadingScheduleCalculator() + private let calculator = ReadingScheduleCalculator() + private let notificationManager = NotificationManager() var body: some View { @@ -136,6 +137,9 @@ struct FinishGoalView: View { // 책 정보 저장하기 if let userBook = userBook { modelContext.insert(userBook) // SwiftData에 새로운 책 저장 + + // 노티 세팅하기 + setNotification(userBook) navigationCoordinator.popToRoot() } else { print("책 정보 없음") @@ -163,7 +167,7 @@ struct FinishGoalView: View { let bookData = UserBook(book: BookDetails(title: book.title, author: book.author, coverURL: book.cover, totalPages: totalPages, startDate: startDate, targetEndDate: endDate, nonReadingDays: bookSettingInputModel.nonReadingDays)) calculator.calculateInitialDailyTargets(for: bookData) userBook = bookData - pagesPerDay = calculator.firstCalculatePagesPerDay(for: bookData) + pagesPerDay = calculator.firstCalculatePagesPerDay(for: bookData).pagesPerDay } } @@ -176,6 +180,14 @@ struct FinishGoalView: View { return dateFormatter.string(from: date) } + private func setNotification(_ readingBook: UserBook) { + notificationManager.clearRequests() + Task { + await self.notificationManager.setupNotifications(notificationType: .morning(readingBook: readingBook)) + + await self.notificationManager.setupNotifications(notificationType: .night(readingBook: readingBook)) + } + } } struct TextView: View { diff --git a/FiveGuyes/FiveGuyes/Sources/Views/Screen/Testing/ReadingTestView.swift b/FiveGuyes/FiveGuyes/Sources/Views/Screen/Testing/ReadingTestView.swift deleted file mode 100644 index 10ed429..0000000 --- a/FiveGuyes/FiveGuyes/Sources/Views/Screen/Testing/ReadingTestView.swift +++ /dev/null @@ -1,184 +0,0 @@ -//// -//// ReadingTestView.swift -//// FiveGuyes -//// -//// Created by zaehorang on 11/7/24. -//// -// -//import SwiftUI -// -////MARK: - 페이지 계산 모델 테스팅을 위한 Text UI입니다. -//struct ReadingTestView: View { -// @State private var readingScheduleCalculator: ReadingScheduleCalculator? = nil -// -// var body: some View { -// ScrollView { -// VStack { -// if let readingProgress = readingScheduleCalculator { -// -// DailyReadingScheduleView(readingScheduleCalculator: readingProgress) -// -// } else { -// // BookInputView로 읽기 목표 설정 화면 -// BookInputView { bookInfo in -// readingScheduleCalculator = ReadingScheduleCalculator(bookInfo: bookInfo) -// } -// .navigationTitle("책 정보 입력") -// } -// } -// } -// } -//} -// -//struct BookInputView: View { -// @State private var title = "" -// @State private var author = "" -// @State private var totalPages: String = "" -// @State private var startDate = Date() -// @State private var endDate = Calendar.current.date(byAdding: .month, value: 1, to: Date()) ?? Date() -// @State private var selectedDate = Date() -// -// @State private var nonReadingDays: [Date] = [] -// -// var onBookInfoSubmit: (BookDetails) -> Void -// -// var body: some View { -// VStack(spacing: 20) { -// TextField("총 페이지 수", text: $totalPages) -// .textFieldStyle(RoundedBorderTextFieldStyle()) -// .keyboardType(.numberPad) -// .padding() -// -// VStack(spacing: 20) { -// Text("제외할 날짜 추가") -// .font(.largeTitle) -// .padding() -// -// // 제외할 날짜 선택 -// DatePicker("제외할 날짜 선택", selection: $selectedDate, displayedComponents: .date) -// .datePickerStyle(GraphicalDatePickerStyle()) -// .padding() -// -// Button { -// // TODO: 여기는 string key로 저장해야 하려나 -// if !nonReadingDays.contains(selectedDate) { -// nonReadingDays.append(selectedDate) -// print("추가: \(selectedDate)") -// } -// } label: { -// Text("제외 날짜 추가") -// .padding() -// .frame(maxWidth: .infinity) -// .background(Color.blue) -// .foregroundColor(.white) -// .cornerRadius(8) -// } -// -// // 제외된 날짜 리스트 표시 -// VStack { -// ForEach(nonReadingDays, id: \.self) { date in -// Text(date.formattedDateString()) -// } -// } -// -// Spacer() -// } -// .padding() -// -// DatePicker("읽기 시작 날짜", selection: $startDate, displayedComponents: .date) -// .padding() -// -// DatePicker("완독 목표 날짜", selection: $endDate, displayedComponents: .date) -// .padding() -// -// Button("읽기 목표 설정") { -// if let total = Int(totalPages) { -// let bookInfo = BookDetails( -// title: title, -// author: author, -// totalPages: total, -// startDate: startDate, -// targetEndDate: endDate, -// nonReadingDays: [] -// ) -// onBookInfoSubmit(bookInfo) -// } -// } -// .padding() -// .background(Color.blue) -// .foregroundColor(.white) -// .cornerRadius(8) -// -// Spacer() -// } -// .padding() -// } -//} -// -//struct DailyReadingScheduleView: View { -// @ObservedObject var readingScheduleCalculator: ReadingScheduleCalculator -// @State private var selectedDate = Date() -// @State private var pagesRead: String = "" -// -// @State private var pagesReadInput: String = "" -// let today = Calendar.current.startOfDay(for: Date()) -// -// var body: some View { -// VStack(spacing: 20) { -// Text("일일 독서 목표") -// .font(.largeTitle) -// .padding() -// -// DatePicker("날짜 선택", selection: $selectedDate, displayedComponents: .date) -// .padding() -// -// VStack { -// ForEach(readingScheduleCalculator.dailyTargets.keys.sorted(), id: \.self) { date in -// HStack { -// Text("\(date)") -// Spacer() -// if let record = readingScheduleCalculator.dailyTargets[date] { -// Text("목표: \(record.targetPages) 페이지") -// Text("읽음: \(record.pagesRead) 페이지") -// } -// } -// -// } -// } -// -// Divider() -// -// TextField("오늘 읽은 페이지 입력", text: $pagesReadInput) -// .textFieldStyle(RoundedBorderTextFieldStyle()) -// .keyboardType(.numberPad) -// .padding() -// -// Button { -// if let pagesRead = Int(pagesReadInput) { -// readingScheduleCalculator.updateReadingProgress(for: today, pagesRead: pagesRead) -// } -// } label: { -// Text("진행 업데이트") -// .padding() -// .frame(maxWidth: .infinity) -// .background(Color.blue) -// .foregroundColor(.white) -// .cornerRadius(8) -// } -// Spacer() -// } -// .padding() -// } -//} -// -//extension Date { -// func formattedDateString() -> String { -// let formatter = DateFormatter() -// formatter.dateFormat = "yyyy-MM-dd" -// return formatter.string(from: self) -// } -//} -// -//#Preview { -// ReadingTestView() -//}