diff --git a/SNUTT-2022/SNUTT.xcodeproj/project.pbxproj b/SNUTT-2022/SNUTT.xcodeproj/project.pbxproj index 35d24d01..1c5fbf75 100644 --- a/SNUTT-2022/SNUTT.xcodeproj/project.pbxproj +++ b/SNUTT-2022/SNUTT.xcodeproj/project.pbxproj @@ -11,7 +11,6 @@ 731C244E2C4442590015877B /* KakaoSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 731C244D2C4442590015877B /* KakaoSDK */; }; 731C24502C4442590015877B /* KakaoSDKAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 731C244F2C4442590015877B /* KakaoSDKAuth */; }; 731C24522C4442590015877B /* KakaoSDKCommon in Frameworks */ = {isa = PBXBuildFile; productRef = 731C24512C4442590015877B /* KakaoSDKCommon */; }; - 73194F0F2C98086C00883EA5 /* ThemeMarketView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73194F0E2C98086C00883EA5 /* ThemeMarketView.swift */; }; 731D9FFD297BC5060027BA25 /* Bookmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 731D9FFC297BC5060027BA25 /* Bookmark.swift */; }; 731DA001297BC54B0027BA25 /* BookmarkDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 731DA000297BC54B0027BA25 /* BookmarkDto.swift */; }; 731DA003297BC5740027BA25 /* BookmarkRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 731DA002297BC5740027BA25 /* BookmarkRouter.swift */; }; @@ -19,6 +18,9 @@ 7329BF252D4BAD4700ABAF23 /* ThemeMarketScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7329BF242D4BAD3F00ABAF23 /* ThemeMarketScene.swift */; }; 734A831F2C2FD41200D6CB95 /* KakaoLogin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 734A831E2C2FD41200D6CB95 /* KakaoLogin.swift */; }; 734B0DE02D2CF23E00A0BAB9 /* View+Gesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 734B0DDF2D2CF23600A0BAB9 /* View+Gesture.swift */; }; + 7359E9122DCF20AB00566337 /* PushNotificationSettingScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7359E9112DCF209500566337 /* PushNotificationSettingScene.swift */; }; + 7359E9142DCF2A9D00566337 /* PushRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7359E9132DCF2A9800566337 /* PushRouter.swift */; }; + 7359E9162DCF2C3200566337 /* PushDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7359E9152DCF2C2F00566337 /* PushDto.swift */; }; 736AF84C2C2F275E00ED9C1A /* GoogleLogin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 736AF84B2C2F275E00ED9C1A /* GoogleLogin.swift */; }; 736AF84F2C2F279900ED9C1A /* GoogleSignIn in Frameworks */ = {isa = PBXBuildFile; productRef = 736AF84E2C2F279900ED9C1A /* GoogleSignIn */; }; 736AF8512C2F279900ED9C1A /* GoogleSignInSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 736AF8502C2F279900ED9C1A /* GoogleSignInSwift */; }; @@ -39,6 +41,9 @@ 73AB84D22C35128B0075DE83 /* IntegrateAccountScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73AB84D12C35128B0075DE83 /* IntegrateAccountScene.swift */; }; 73AB84D42C3514080075DE83 /* IntegrateAccountViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73AB84D32C3514080075DE83 /* IntegrateAccountViewModel.swift */; }; 73B6E0882CCB34B2006DD4F0 /* SocialProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73B6E0862CCB34AA006DD4F0 /* SocialProvider.swift */; }; + 73C930E02DD0D30F0091F08C /* PushRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73C930DF2DD0D3030091F08C /* PushRepository.swift */; }; + 73C930E22DD0D47F0091F08C /* PushService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73C930E12DD0D47C0091F08C /* PushService.swift */; }; + 73C930E42DD0D8040091F08C /* PushState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73C930E32DD0D8020091F08C /* PushState.swift */; }; 73EF6E682D3A5F330050D5C4 /* AppleLogin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73EF6E672D3A5F2D0050D5C4 /* AppleLogin.swift */; }; B800A38B2B76132C008E8D84 /* SearchTimeMaskDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = B800A38A2B76132C008E8D84 /* SearchTimeMaskDto.swift */; }; B800A38C2B77BD78008E8D84 /* SearchTimeMaskDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = B800A38A2B76132C008E8D84 /* SearchTimeMaskDto.swift */; }; @@ -379,6 +384,9 @@ 7329BF242D4BAD3F00ABAF23 /* ThemeMarketScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeMarketScene.swift; sourceTree = ""; }; 734A831E2C2FD41200D6CB95 /* KakaoLogin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KakaoLogin.swift; sourceTree = ""; }; 734B0DDF2D2CF23600A0BAB9 /* View+Gesture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Gesture.swift"; sourceTree = ""; }; + 7359E9112DCF209500566337 /* PushNotificationSettingScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationSettingScene.swift; sourceTree = ""; }; + 7359E9132DCF2A9800566337 /* PushRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushRouter.swift; sourceTree = ""; }; + 7359E9152DCF2C2F00566337 /* PushDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushDto.swift; sourceTree = ""; }; 736AF84B2C2F275E00ED9C1A /* GoogleLogin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleLogin.swift; sourceTree = ""; }; 738406ED2B57107C00007E62 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; }; 738406F02B5710C200007E62 /* ThemeDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeDto.swift; sourceTree = ""; }; @@ -395,6 +403,9 @@ 73AB84D12C35128B0075DE83 /* IntegrateAccountScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrateAccountScene.swift; sourceTree = ""; }; 73AB84D32C3514080075DE83 /* IntegrateAccountViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrateAccountViewModel.swift; sourceTree = ""; }; 73B6E0862CCB34AA006DD4F0 /* SocialProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialProvider.swift; sourceTree = ""; }; + 73C930DF2DD0D3030091F08C /* PushRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushRepository.swift; sourceTree = ""; }; + 73C930E12DD0D47C0091F08C /* PushService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushService.swift; sourceTree = ""; }; + 73C930E32DD0D8020091F08C /* PushState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushState.swift; sourceTree = ""; }; 73EF6E672D3A5F2D0050D5C4 /* AppleLogin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleLogin.swift; sourceTree = ""; }; B800A38A2B76132C008E8D84 /* SearchTimeMaskDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTimeMaskDto.swift; sourceTree = ""; }; B80240B12CE8564500178428 /* TimerModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerModifier.swift; sourceTree = ""; }; @@ -721,6 +732,7 @@ B87DF6F82918B7AD008BB95B /* PopupState.swift */, CE98204A2A09FBDD001037F5 /* DebugState.swift */, 738406F82B57154400007E62 /* ThemeState.swift */, + 73C930E32DD0D8020091F08C /* PushState.swift */, ); path = States; sourceTree = ""; @@ -734,6 +746,7 @@ B8F40EAC28980D840021A2A9 /* TimetableSettingScene.swift */, 738406FA2B57173400007E62 /* ThemeSettingScene.swift */, 73AB84D12C35128B0075DE83 /* IntegrateAccountScene.swift */, + 7359E9112DCF209500566337 /* PushNotificationSettingScene.swift */, ); path = Settings; sourceTree = ""; @@ -972,6 +985,7 @@ CE4777F42A6ADCE200E03253 /* VacancyRouter.swift */, 738406F42B5712BF00007E62 /* ThemeRouter.swift */, B8823BC22BC2ED41003A3B69 /* BuildingRouter.swift */, + 7359E9132DCF2A9800566337 /* PushRouter.swift */, ); path = Router; sourceTree = ""; @@ -1063,6 +1077,7 @@ CEDDCA832A6AFBF700474D4E /* VacancyDto.swift */, CE17DF8A2A7E96E1000432B8 /* ConfigDto.swift */, 738406F02B5710C200007E62 /* ThemeDto.swift */, + 7359E9152DCF2C2F00566337 /* PushDto.swift */, ); path = Dto; sourceTree = ""; @@ -1107,6 +1122,7 @@ BE6D892B28EFC76E000607A6 /* EtcService.swift */, CE3384B92A86704100437CC5 /* FriendsService.swift */, 738406F62B57134300007E62 /* ThemeService.swift */, + 73C930E12DD0D47C0091F08C /* PushService.swift */, ); path = Services; sourceTree = ""; @@ -1132,6 +1148,7 @@ DC1E0ECE28772F13005632A3 /* NetworkUtils.swift */, B8E51E6528B5EC500065248E /* NetworkConfiguration.swift */, 738406F22B57119100007E62 /* ThemeRepository.swift */, + 73C930DF2DD0D3030091F08C /* PushRepository.swift */, ); path = Repositories; sourceTree = ""; @@ -1595,6 +1612,7 @@ CE17DF912A7F43E0000432B8 /* VacancySugangSnuButton.swift in Sources */, CE7243612B30240D00F9E0D7 /* InteractiveDismissKeyboardModifier.swift in Sources */, BE1D2B3A28014527008F9134 /* Weekday.swift in Sources */, + 73C930E22DD0D47F0091F08C /* PushService.swift in Sources */, BEDE34CA28754F3100525014 /* Sheet.swift in Sources */, BEB3B6B128D4D4D900E56062 /* View+ResignResponder.swift in Sources */, 734B0DE02D2CF23E00A0BAB9 /* View+Gesture.swift in Sources */, @@ -1619,6 +1637,7 @@ BE28036A28E93EEF00B2B1AB /* LoginScene.swift in Sources */, BECDB89A2840CFAA00F62AC8 /* MenuSheetScene.swift in Sources */, DCD41A6327E5CCD500CF380E /* TimetableService.swift in Sources */, + 73C930E42DD0D8040091F08C /* PushState.swift in Sources */, DC9C2CAB287805D3009EC3BC /* TimetableDto.swift in Sources */, BE682BFD2887FC78009EBCB7 /* SearchTag.swift in Sources */, BE9413DD28C3B68F00171060 /* SearchTagsScrollView.swift in Sources */, @@ -1626,6 +1645,7 @@ B88D16FB28AE2BB900E2D652 /* UserRouter.swift in Sources */, BE982FC0281D762F005F71E6 /* TimetableZStack.swift in Sources */, B87B316C28D755B6005C170B /* SystemState.swift in Sources */, + 73C930E02DD0D30F0091F08C /* PushRepository.swift in Sources */, B8F0B2322A540A5300A2E15E /* EmailVerificationView.swift in Sources */, CE9820502A0A0BB7001037F5 /* NetworkLogListScene.swift in Sources */, BE982FBC281D5227005F71E6 /* TimePlace.swift in Sources */, @@ -1637,11 +1657,13 @@ CEA428A02D71E2DC00944DAB /* AnalyticsScreenModifier.swift in Sources */, B800A38B2B76132C008E8D84 /* SearchTimeMaskDto.swift in Sources */, CEF630942BC690DF00D26999 /* FloatingButton.swift in Sources */, + 7359E9142DCF2A9D00566337 /* PushRouter.swift in Sources */, B87DF6ED2914FAFB008BB95B /* PopupRouter.swift in Sources */, BE682BE328870EBD009EBCB7 /* LectureDetailViewModel.swift in Sources */, BE682BD928870718009EBCB7 /* LectureRepository.swift in Sources */, BE98A00A288A90C000C2CE95 /* Logo.swift in Sources */, BE982FB8281D0B33005F71E6 /* TimetableBlocksLayer.swift in Sources */, + 7359E9122DCF20AB00566337 /* PushNotificationSettingScene.swift in Sources */, BEEBDFBC286B408E00DB5976 /* LectureBlocks.swift in Sources */, BE77D09E28AEAA5A0067A9D8 /* CourseBookRouter.swift in Sources */, CE076E002A09661400C9430B /* NetworkLogEntry.swift in Sources */, @@ -1727,6 +1749,7 @@ B8F40EB328980DFF0021A2A9 /* PrivacyPolicyView.swift in Sources */, 738407012B571D8E00007E62 /* ThemeIcon.swift in Sources */, B8A72A022A8F1ED000094FAA /* ChangeNicknameView.swift in Sources */, + 7359E9162DCF2C3200566337 /* PushDto.swift in Sources */, CE98750C29CC527F007F063A /* TimeRangeSlider.swift in Sources */, B8F40EB728980E7A0021A2A9 /* AccountSettingViewModel.swift in Sources */, BEB3B6AF28D4D49A00E56062 /* DetailLabel.swift in Sources */, diff --git a/SNUTT-2022/SNUTT/AppState/AppEnvironment.swift b/SNUTT-2022/SNUTT/AppState/AppEnvironment.swift index 76b12d7d..5db0ede6 100644 --- a/SNUTT-2022/SNUTT/AppState/AppEnvironment.swift +++ b/SNUTT-2022/SNUTT/AppState/AppEnvironment.swift @@ -28,6 +28,7 @@ extension AppEnvironment { let vacancyService: VacancyServiceProtocol let friendsService: FriendsServiceProtocol let themeService: ThemeServiceProtocol + let pushService: PushServiceProtocol } } @@ -46,6 +47,7 @@ extension AppEnvironment { let vacancyRepository: VacancyRepositoryProtocol let configRepository: ConfigRepositoryProtocol let themeRepository: ThemeRepositoryProtocol + let pushRepository: PushRepositoryProtocol } struct LocalRepositories { @@ -105,6 +107,7 @@ extension AppEnvironment { let vacancyRepository = VacancyRepository(session: session) let configRepository = ConfigRepository(session: session) let themeRepository = ThemeRepository(session: session) + let pushRepository = PushRepository(session: session) return .init(timetableRepository: timetableRepository, userRepository: userRepository, @@ -118,7 +121,8 @@ extension AppEnvironment { etcRepository: etcRepository, vacancyRepository: vacancyRepository, configRepository: configRepository, - themeRepository: themeRepository) + themeRepository: themeRepository, + pushRepository: pushRepository) } private static func configuredDBRepositories(appState _: AppState) -> LocalRepositories { @@ -184,6 +188,10 @@ extension AppEnvironment { webRepositories: webRepositories, localRepositories: localRepositories ) + let pushService = PushService( + appState: appState, + webRepositories: webRepositories + ) return .init(timetableService: timetableService, userService: userService, lectureService: lectureService, @@ -196,7 +204,8 @@ extension AppEnvironment { etcService: etcService, vacancyService: vacancyService, friendsService: friendsService, - themeService: themeService) + themeService: themeService, + pushService: pushService) } } @@ -226,7 +235,8 @@ extension EnvironmentValues { etcService: FakeEtcService(), vacancyService: FakeVacancyService(), friendsService: FakeFriendsService(), - themeService: FakeThemeService()) + themeService: FakeThemeService(), + pushService: FakePushService()) } } #endif diff --git a/SNUTT-2022/SNUTT/AppState/AppState.swift b/SNUTT-2022/SNUTT/AppState/AppState.swift index d38c35bc..95cf58e0 100644 --- a/SNUTT-2022/SNUTT/AppState/AppState.swift +++ b/SNUTT-2022/SNUTT/AppState/AppState.swift @@ -24,6 +24,7 @@ final class AppState { var routing = ViewRoutingState() var vacancy = VacancyState() var theme = ThemeState() + var push = PushState() #if DEBUG var debug = DebugState() @@ -35,7 +36,10 @@ final class AppState { static var preview: AppState { let state = AppState() state.timetable.current = .preview - state.search.selectedTagList = [.init(id: .init(), type: .classification, text: "예시1"), .init(id: .init(), type: .credit, text: "예시2")] + state.search.selectedTagList = [ + .init(id: .init(), type: .classification, text: "예시1"), + .init(id: .init(), type: .credit, text: "예시2"), + ] return state } } diff --git a/SNUTT-2022/SNUTT/AppState/States/PushState.swift b/SNUTT-2022/SNUTT/AppState/States/PushState.swift new file mode 100644 index 00000000..6f481a8f --- /dev/null +++ b/SNUTT-2022/SNUTT/AppState/States/PushState.swift @@ -0,0 +1,38 @@ +// +// PushState.swift +// SNUTT +// +// Created by 이채민 on 5/11/25. +// + +import Combine +import Foundation + +struct PushNotificationOptions: OptionSet, Codable { + let rawValue: Int8 + init(rawValue: Int8) { self.rawValue = rawValue } + + static let lectureUpdate = PushNotificationOptions(rawValue: 1 << 0) + static let vacancy = PushNotificationOptions(rawValue: 1 << 1) + static let `default`: PushNotificationOptions = [.lectureUpdate, .vacancy] +} + +class PushState: ObservableObject { + @Published var options: PushNotificationOptions = .default + + var isLectureUpdatePushOn: Bool { + get { options.contains(.lectureUpdate) } + set { + if newValue { options.insert(.lectureUpdate) } + else { options.remove(.lectureUpdate) } + } + } + + var isVacancyPushOn: Bool { + get { options.contains(.vacancy) } + set { + if newValue { options.insert(.vacancy) } + else { options.remove(.vacancy) } + } + } +} diff --git a/SNUTT-2022/SNUTT/Repositories/Dto/PushDto.swift b/SNUTT-2022/SNUTT/Repositories/Dto/PushDto.swift new file mode 100644 index 00000000..c9196d31 --- /dev/null +++ b/SNUTT-2022/SNUTT/Repositories/Dto/PushDto.swift @@ -0,0 +1,17 @@ +// +// PushDto.swift +// SNUTT +// +// Created by 이채민 on 5/10/25. +// + +import Foundation + +struct PushDto: Codable { + let pushPreferences: [PushPreferenceDto] +} + +struct PushPreferenceDto: Codable { + let type: String + let isEnabled: Bool +} diff --git a/SNUTT-2022/SNUTT/Repositories/PushRepository.swift b/SNUTT-2022/SNUTT/Repositories/PushRepository.swift new file mode 100644 index 00000000..6c3b7b8e --- /dev/null +++ b/SNUTT-2022/SNUTT/Repositories/PushRepository.swift @@ -0,0 +1,37 @@ +// +// PushRepository.swift +// SNUTT +// +// Created by 이채민 on 5/11/25. +// + +import Alamofire +import Foundation + +protocol PushRepositoryProtocol { + func getPreference() async throws -> PushDto + func updatePreference(lectureUpdate: PushPreferenceDto, vacancy: PushPreferenceDto) async throws +} + +class PushRepository: PushRepositoryProtocol { + private let session: Session + + init(session: Session) { + self.session = session + } + + func getPreference() async throws -> PushDto { + return try await session + .request(PushRouter.getPreference) + .serializingDecodable(PushDto.self) + .handlingError() + } + + func updatePreference(lectureUpdate: PushPreferenceDto, vacancy: PushPreferenceDto) async throws { + let preferences = [lectureUpdate, vacancy] + let _ = try await session + .request(PushRouter.updatePreference(preferences: preferences)) + .serializingString(emptyResponseCodes: [200]) + .handlingError() + } +} diff --git a/SNUTT-2022/SNUTT/Repositories/Router/PushRouter.swift b/SNUTT-2022/SNUTT/Repositories/Router/PushRouter.swift new file mode 100644 index 00000000..9eb6c83b --- /dev/null +++ b/SNUTT-2022/SNUTT/Repositories/Router/PushRouter.swift @@ -0,0 +1,61 @@ +// +// PushRouter.swift +// SNUTT +// +// Created by 이채민 on 5/10/25. +// + +import Alamofire +import Foundation + +enum PushRouter: Router { + var baseURL: URL { return URL(string: NetworkConfiguration.serverV1BaseURL + "/push")! } + + case getPreference + case updatePreference(preferences: [PushPreferenceDto]) + + var method: HTTPMethod { + switch self { + case .getPreference: + return .get + case .updatePreference: + return .post + } + } + + var path: String { + switch self { + case .getPreference: + return "/preferences" + case .updatePreference: + return "/preferences" + } + } + + var parameters: Parameters? { + switch self { + case .getPreference: + return nil + case let .updatePreference(preferences): + return ["pushPreferences": preferences.encodeToJSONArray() ?? []] + } + } +} + +extension Encodable { + public func asAnyDictionary() -> [String: Any]? { + guard let data = try? JSONEncoder().encode(self), + let jsonObject = try? JSONSerialization.jsonObject(with: data), + let dictionary = jsonObject as? [String: Any] + else { + return nil + } + return dictionary + } +} + +extension Array where Element: Encodable { + public func encodeToJSONArray() -> [[String: Any]]? { + return compactMap { $0.asAnyDictionary() } + } +} diff --git a/SNUTT-2022/SNUTT/Services/PushService.swift b/SNUTT-2022/SNUTT/Services/PushService.swift new file mode 100644 index 00000000..33e0d2f0 --- /dev/null +++ b/SNUTT-2022/SNUTT/Services/PushService.swift @@ -0,0 +1,59 @@ +// +// PushService.swift +// SNUTT +// +// Created by 이채민 on 5/11/25. +// + +import Foundation +import SwiftUI + +@MainActor +protocol PushServiceProtocol: Sendable { + func getPreference() async throws + func updatePreference(options: PushNotificationOptions) async throws +} + +struct PushService: PushServiceProtocol { + var appState: AppState + var webRepositories: AppEnvironment.WebRepositories + + func getPreference() async throws { + let dto = try await pushRepository.getPreference() + var options: PushNotificationOptions = [] + + for p in dto.pushPreferences { + switch p.type { + case "LECTURE_UPDATE" where p.isEnabled: + options.insert(.lectureUpdate) + case "VACANCY_NOTIFICATION" where p.isEnabled: + options.insert(.vacancy) + default: + break + } + } + appState.push.options = options.isEmpty ? .default : options + } + + func updatePreference(options: PushNotificationOptions) async throws { + let lectureUpdate = PushPreferenceDto( + type: "LECTURE_UPDATE", + isEnabled: options.contains(.lectureUpdate) + ) + let vacancyUpdate = PushPreferenceDto( + type: "VACANCY_NOTIFICATION", + isEnabled: options.contains(.vacancy) + ) + + try await pushRepository.updatePreference(lectureUpdate: lectureUpdate, vacancy: vacancyUpdate) + } + + private var pushRepository: PushRepositoryProtocol { + webRepositories.pushRepository + } +} + +class FakePushService: PushServiceProtocol { + func getPreference() async throws {} + func updatePreference(options _: PushNotificationOptions) async throws {} +} diff --git a/SNUTT-2022/SNUTT/ViewModels/SettingViewModel.swift b/SNUTT-2022/SNUTT/ViewModels/SettingViewModel.swift index 6d0108e0..8d4a8187 100644 --- a/SNUTT-2022/SNUTT/ViewModels/SettingViewModel.swift +++ b/SNUTT-2022/SNUTT/ViewModels/SettingViewModel.swift @@ -115,6 +115,14 @@ class SettingViewModel: BaseViewModel, ObservableObject { } } + func getPushPreference() async { + do { + try await services.pushService.getPreference() + } catch { + services.globalUIService.presentErrorAlert(error: error) + } + } + func closeBottomSheet() { services.themeService.closeBottomSheet() } diff --git a/SNUTT-2022/SNUTT/Views/Scenes/Settings/PushNotificationSettingScene.swift b/SNUTT-2022/SNUTT/Views/Scenes/Settings/PushNotificationSettingScene.swift new file mode 100644 index 00000000..0a08b46e --- /dev/null +++ b/SNUTT-2022/SNUTT/Views/Scenes/Settings/PushNotificationSettingScene.swift @@ -0,0 +1,66 @@ +// +// PushNotificationSettingScene.swift +// SNUTT +// +// Created by 이채민 on 5/10/25. +// + +import Combine +import SwiftUI + +struct PushNotificationSettingScene: View { + @ObservedObject var viewModel: ViewModel + var body: some View { + Form { + Section { + Group { + Toggle("강의 업데이트", isOn: $viewModel.isLectureUpdatePushOn) + .animation(.easeInOut, value: viewModel.isLectureUpdatePushOn) + Toggle("빈자리 알림", isOn: $viewModel.isVacancyPushOn) + .animation(.easeInOut, value: viewModel.isVacancyPushOn) + } + } + } + .navigationTitle("푸시알림 설정") + .navigationBarTitleDisplayMode(.inline) + } +} + +extension PushNotificationSettingScene { + class ViewModel: BaseViewModel, ObservableObject { + @Published private var _options: PushNotificationOptions = .default + private var cancellables = Set() + + var isLectureUpdatePushOn: Bool { + get { _options.contains(.lectureUpdate) } + set { updateOption(.lectureUpdate, enabled: newValue) } + } + + var isVacancyPushOn: Bool { + get { _options.contains(.vacancy) } + set { updateOption(.vacancy, enabled: newValue) } + } + + override init(container: DIContainer) { + super.init(container: container) + appState.push.$options + .receive(on: DispatchQueue.main) + .assign(to: \._options, on: self) + .store(in: &cancellables) + } + + private func updateOption(_ option: PushNotificationOptions, enabled: Bool) { + if enabled { _options.insert(option) } + else { _options.remove(option) } + Task { await updatePushPreference() } + } + + func updatePushPreference() async { + do { + try await services.pushService.updatePreference(options: _options) + } catch { + services.globalUIService.presentErrorAlert(error: error) + } + } + } +} diff --git a/SNUTT-2022/SNUTT/Views/Scenes/Settings/SettingScene.swift b/SNUTT-2022/SNUTT/Views/Scenes/Settings/SettingScene.swift index a6e44a0b..c864e13b 100644 --- a/SNUTT-2022/SNUTT/Views/Scenes/Settings/SettingScene.swift +++ b/SNUTT-2022/SNUTT/Views/Scenes/Settings/SettingScene.swift @@ -49,6 +49,9 @@ struct SettingScene: View { } Section { + SettingsLinkItem(title: "푸시알림 설정") { + PushNotificationSettingScene(viewModel: .init(container: viewModel.container)) + } SettingsLinkItem(title: "빈자리 알림", isActive: $viewModel.routingState.pushToVacancy) { VacancyScene(viewModel: .init(container: viewModel.container)) } @@ -118,6 +121,7 @@ struct SettingScene: View { await viewModel.fetchUser() await viewModel.fetchSocialProvider() await viewModel.getThemeList() + await viewModel.getPushPreference() } let _ = debugChanges()