diff --git a/HongikYeolgong2.xcodeproj/project.pbxproj b/HongikYeolgong2.xcodeproj/project.pbxproj index 9435075..d3f5065 100644 --- a/HongikYeolgong2.xcodeproj/project.pbxproj +++ b/HongikYeolgong2.xcodeproj/project.pbxproj @@ -57,6 +57,7 @@ 4780044C2CCAAD4F00FFAF00 /* WeekDay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4780044B2CCAAD4F00FFAF00 /* WeekDay.swift */; }; 4780044E2CCAAE3200FFAF00 /* String+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4780044D2CCAAE3200FFAF00 /* String+.swift */; }; 478004502CCABEFF00FFAF00 /* WeeklyStudyRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4780044F2CCABEFF00FFAF00 /* WeeklyStudyRecord.swift */; }; + 478589FE2CE4973B0027ED32 /* MenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 478589FD2CE4973B0027ED32 /* MenuItem.swift */; }; 4786A0EC2CC9E2BC008635A4 /* UserPermissionsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4786A0EB2CC9E2BC008635A4 /* UserPermissionsInteractor.swift */; }; 4786A1052CCA3352008635A4 /* StudySessionInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4786A1042CCA3352008635A4 /* StudySessionInteractor.swift */; }; 4786A1072CCA33DF008635A4 /* WeeklyEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4786A1062CCA33DF008635A4 /* WeeklyEndpoint.swift */; }; @@ -108,6 +109,7 @@ 47B1D4C72C9CB1760071B62B /* HongikYeolgong2UITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47B1D4C62C9CB1760071B62B /* HongikYeolgong2UITests.swift */; }; 47B1D4C92C9CB1760071B62B /* HongikYeolgong2UITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47B1D4C82C9CB1760071B62B /* HongikYeolgong2UITestsLaunchTests.swift */; }; 47BACCF72CA164BA00295DAC /* Font+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47BACCF62CA164BA00295DAC /* Font+.swift */; }; + 47BB5BE32CE52858002BBEE1 /* Page.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47BB5BE22CE52858002BBEE1 /* Page.swift */; }; 47BE30E32CC813BB0015D973 /* KeyChainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47BE30E22CC813BB0015D973 /* KeyChainManager.swift */; }; 47BE30E52CC81A9E0015D973 /* URLRequest+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47BE30E42CC81A9E0015D973 /* URLRequest+.swift */; }; 47C815382CE21E640017EA24 /* ASAuthEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47C815372CE21E640017EA24 /* ASAuthEndpoint.swift */; }; @@ -236,6 +238,7 @@ 4780044B2CCAAD4F00FFAF00 /* WeekDay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeekDay.swift; sourceTree = ""; }; 4780044D2CCAAE3200FFAF00 /* String+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+.swift"; sourceTree = ""; }; 4780044F2CCABEFF00FFAF00 /* WeeklyStudyRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeeklyStudyRecord.swift; sourceTree = ""; }; + 478589FD2CE4973B0027ED32 /* MenuItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuItem.swift; sourceTree = ""; }; 4786A0EB2CC9E2BC008635A4 /* UserPermissionsInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPermissionsInteractor.swift; sourceTree = ""; }; 4786A1042CCA3352008635A4 /* StudySessionInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StudySessionInteractor.swift; sourceTree = ""; }; 4786A1062CCA33DF008635A4 /* WeeklyEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeeklyEndpoint.swift; sourceTree = ""; }; @@ -292,6 +295,7 @@ 47B1D4C62C9CB1760071B62B /* HongikYeolgong2UITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HongikYeolgong2UITests.swift; sourceTree = ""; }; 47B1D4C82C9CB1760071B62B /* HongikYeolgong2UITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HongikYeolgong2UITestsLaunchTests.swift; sourceTree = ""; }; 47BACCF62CA164BA00295DAC /* Font+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Font+.swift"; sourceTree = ""; }; + 47BB5BE22CE52858002BBEE1 /* Page.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Page.swift; sourceTree = ""; }; 47BE30E22CC813BB0015D973 /* KeyChainManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyChainManager.swift; sourceTree = ""; }; 47BE30E42CC81A9E0015D973 /* URLRequest+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLRequest+.swift"; sourceTree = ""; }; 47C815372CE21E640017EA24 /* ASAuthEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASAuthEndpoint.swift; sourceTree = ""; }; @@ -526,6 +530,14 @@ path = Mapper; sourceTree = ""; }; + 478589FC2CE497100027ED32 /* Component */ = { + isa = PBXGroup; + children = ( + 478589FD2CE4973B0027ED32 /* MenuItem.swift */, + ); + path = Component; + sourceTree = ""; + }; 4786A10A2CCA3496008635A4 /* StudySession */ = { isa = PBXGroup; children = ( @@ -676,6 +688,7 @@ 47A147372CA1379A00A91F66 /* Setting */ = { isa = PBXGroup; children = ( + 478589FC2CE497100027ED32 /* Component */, 475B86E22CA1AF40000534B2 /* SettingView.swift */, ); path = Setting; @@ -984,6 +997,7 @@ 4780044B2CCAAD4F00FFAF00 /* WeekDay.swift */, 47A540722CD0D90F00DC40D0 /* Nickname.swift */, 479BF66B2CDD1A8D009D9E44 /* StudyNotificationType.swift */, + 47BB5BE22CE52858002BBEE1 /* Page.swift */, ); path = Models; sourceTree = ""; @@ -1231,6 +1245,7 @@ 4707230F2CBC1A9C0046469F /* LoginRequestDTO.swift in Sources */, 478F84442CD350850097CAA1 /* WeeklyRankingResponseDTO.swift in Sources */, 473E8EBC2CCEBEF3000F102C /* ModalView.swift in Sources */, + 47BB5BE32CE52858002BBEE1 /* Page.swift in Sources */, 473E8EB62CCE6A02000F102C /* Date+.swift in Sources */, 47C815412CE221060017EA24 /* SocialLoginRepositoryImpl.swift in Sources */, 470483A82CDB50FA00C381ED /* TokenEndpoint.swift in Sources */, @@ -1292,6 +1307,7 @@ 47F71B5D2CDC77C60044DEC5 /* UserDataInteractor+Migration.swift in Sources */, 4763FFB52CB90EBD00990336 /* InitialView.swift in Sources */, 47E250712CCF274400267897 /* WeeklyStudyInteractor.swift in Sources */, + 478589FE2CE4973B0027ED32 /* MenuItem.swift in Sources */, 47C815462CE223DA0017EA24 /* ASTokenResponseDTO.swift in Sources */, 470723092CBC198E0046469F /* AuthRepository.swift in Sources */, 47F4F6972CC88FBB00543D24 /* SignUpRequestDTO.swift in Sources */, @@ -1497,7 +1513,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 7; DEVELOPMENT_ASSET_PATHS = "\"HongikYeolgong2/Resources/Preview Content\""; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = P4D4ZQC4YF; @@ -1544,7 +1560,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 7; DEVELOPMENT_ASSET_PATHS = "\"HongikYeolgong2/Resources/Preview Content\""; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = P4D4ZQC4YF; diff --git a/HongikYeolgong2/Core/AppEnviroment.swift b/HongikYeolgong2/Core/AppEnviroment.swift index 7274327..e051f43 100644 --- a/HongikYeolgong2/Core/AppEnviroment.swift +++ b/HongikYeolgong2/Core/AppEnviroment.swift @@ -77,7 +77,9 @@ extension AppEnviroment { userPermissionsInteractor: RealUserPermissionsInteractor( appState: appState, openAppSetting: { - if let url = URL(string: UIApplication.openNotificationSettingsURLString) { + guard let url = URL(string: UIApplication.openSettingsURLString) else { return } + + if UIApplication.shared.canOpenURL(url) { UIApplication.shared.open(url) } } diff --git a/HongikYeolgong2/Data/Repositories/Auth/AuthRepositoryImpl.swift b/HongikYeolgong2/Data/Repositories/Auth/AuthRepositoryImpl.swift index c840fd0..ee138f9 100644 --- a/HongikYeolgong2/Data/Repositories/Auth/AuthRepositoryImpl.swift +++ b/HongikYeolgong2/Data/Repositories/Auth/AuthRepositoryImpl.swift @@ -13,13 +13,13 @@ final class AuthRepositoryImpl: AuthRepository { /// 소셜로그인 /// - Parameter loginReqDto: 로그인 요청 DTO(이메일, identityToken) /// - Returns: 로그인 응답 DTO(accessToken, 가입여부) - func signIn(loginReqDto: LoginRequestDTO) -> AnyPublisher { - return Future { promise in + func signIn(loginReqDto: LoginRequestDTO) -> AnyPublisher { + return Future { promise in Task { do { let response: BaseResponse = try await NetworkService.shared.request(endpoint: AuthEndpoint.login(loginReqDto: loginReqDto)) promise(.success(response.data)) - } catch let error as NetworkError { + } catch let error as NetworkError { promise(.failure(error)) } } @@ -50,8 +50,8 @@ final class AuthRepositoryImpl: AuthRepository { Task { do { let response: BaseResponse = try await NetworkService.shared.request(endpoint: UserEndpoint.signUp(signUpReqDto: signUpReqDto)) - promise(.success(response.data)) - } catch let error as NetworkError { + promise(.success(response.data)) + } catch let error as NetworkError { promise(.failure(error)) } } @@ -66,7 +66,7 @@ final class AuthRepositoryImpl: AuthRepository { do { let response: BaseResponse = try await NetworkService.shared.request(endpoint: UserEndpoint.getUser) promise(.success(response.data)) - } catch let error as NetworkError { + } catch let error as NetworkError { promise(.failure(error)) } } @@ -98,7 +98,6 @@ final class AuthRepositoryImpl: AuthRepository { let response: BaseResponse = try await NetworkService.shared.request(endpoint: UserEndpoint.getUserProfile) promise(.success(response.data)) } catch let error as NetworkError { - promise(.failure(error)) } } @@ -110,10 +109,8 @@ final class AuthRepositoryImpl: AuthRepository { Task { do { let response: BaseResponse = try await NetworkService.shared.request(endpoint: AuthEndpoint.withdraw) - promise(.success(())) - } catch let error as NetworkError { - + } catch let error as NetworkError { promise(.failure(error)) } } diff --git a/HongikYeolgong2/Data/Repositories/Auth/SocialLoginRepositoryImpl.swift b/HongikYeolgong2/Data/Repositories/Auth/SocialLoginRepositoryImpl.swift index e535239..663e0d0 100644 --- a/HongikYeolgong2/Data/Repositories/Auth/SocialLoginRepositoryImpl.swift +++ b/HongikYeolgong2/Data/Repositories/Auth/SocialLoginRepositoryImpl.swift @@ -15,7 +15,7 @@ final class SocialLoginRepositoryImpl: SocialLoginRepository { let _ = try await NetworkService.shared.plainRequest(endpoint: ASAuthEndpoint.requestRevoke(asRevokeTokenRequestDto)) promise(.success(())) } - catch let error as NetworkError { + catch let error as NetworkError { promise(.failure(error)) } } diff --git a/HongikYeolgong2/Data/Repositories/StudySession/StudySessionRepositoryImpl.swift b/HongikYeolgong2/Data/Repositories/StudySession/StudySessionRepositoryImpl.swift index 9096345..acde8ab 100644 --- a/HongikYeolgong2/Data/Repositories/StudySession/StudySessionRepositoryImpl.swift +++ b/HongikYeolgong2/Data/Repositories/StudySession/StudySessionRepositoryImpl.swift @@ -43,7 +43,6 @@ final class StudySessionRepositoryImpl: StudySessionRepository { let response: BaseResponse = try await NetworkService.shared.request(endpoint: WeeklyEndpoint.getWiseSaying) promise(.success(response.data)) } catch let error as NetworkError { - print(error.message) promise(.failure(error)) } } @@ -57,7 +56,7 @@ final class StudySessionRepositoryImpl: StudySessionRepository { let response: BaseResponse = try await NetworkService.shared.request(endpoint: WeeklyEndpoint.getWeeklyRanking(yearWeek: weekNumber)) promise(.success(response.data.toEntity())) } catch let error as NetworkError { - print(error.message) + promise(.failure(error)) } } }.eraseToAnyPublisher() diff --git a/HongikYeolgong2/Data/Repositories/Weekly/WeeklyRepositoryImpl.swift b/HongikYeolgong2/Data/Repositories/Weekly/WeeklyRepositoryImpl.swift index d3ed70d..664c0e1 100644 --- a/HongikYeolgong2/Data/Repositories/Weekly/WeeklyRepositoryImpl.swift +++ b/HongikYeolgong2/Data/Repositories/Weekly/WeeklyRepositoryImpl.swift @@ -15,7 +15,6 @@ final class WeeklyRepositoryImpl: WeeklyRepository { let response: BaseResponse = try await NetworkService.shared.request(endpoint: WeeklyEndpoint.getWeekField(date: date)) promise(.success(response.data.weekNumber)) } catch let error as NetworkError { - print(error.message) promise(.failure(error)) } } diff --git a/HongikYeolgong2/Domain/Interactors/StudySessionInteractor.swift b/HongikYeolgong2/Domain/Interactors/StudySessionInteractor.swift index bf01325..1e1ab40 100644 --- a/HongikYeolgong2/Domain/Interactors/StudySessionInteractor.swift +++ b/HongikYeolgong2/Domain/Interactors/StudySessionInteractor.swift @@ -140,6 +140,7 @@ final class StudySessionInteractorImpl: StudySessionInteractor { /// 열람실 이용종료 Notification을 등록합니다. func registerNotification(for type: StudyNotificationType, endTimeInMinute: TimeInterval) { + guard appState.value.userData.isOnAlarm else { return } let content = configuredNotificationContent(for: type) let trigger = configuredNotificationTrigger(for: type, endTime: endTimeInMinute) let request = UNNotificationRequest( diff --git a/HongikYeolgong2/Domain/Interactors/UserDataInteractor+Migration.swift b/HongikYeolgong2/Domain/Interactors/UserDataInteractor+Migration.swift index b7c6421..6a4c12d 100644 --- a/HongikYeolgong2/Domain/Interactors/UserDataInteractor+Migration.swift +++ b/HongikYeolgong2/Domain/Interactors/UserDataInteractor+Migration.swift @@ -78,7 +78,7 @@ final class UserDataMigrationInteractor: UserDataInteractor { /// - Parameter authorization: ASAuthorization func requestAppleLogin(_ authorization: ASAuthorization) { guard let appleIDCredential = appleLoginService.requestAppleLogin(authorization), - let idTokenData = appleIDCredential.identityToken, + let idTokenData = appleIDCredential.identityToken, let idToken = String(data: idTokenData, encoding: .utf8) else { return } @@ -89,12 +89,14 @@ final class UserDataMigrationInteractor: UserDataInteractor { return Fail(error: NetworkError.decodingError("")).eraseToAnyPublisher() } let loginReqDto: LoginRequestDTO = .init(email: userID, idToken: idToken) + return authRepository.signIn(loginReqDto: loginReqDto) } .receive(on: DispatchQueue.main) .sink( receiveCompletion: { _ in}, receiveValue: { [weak self] loginResDto in + guard let self = self else { return } let isAlreadyExists = loginResDto.alreadyExist @@ -124,7 +126,8 @@ final class UserDataMigrationInteractor: UserDataInteractor { receiveValue: { [weak self] signUpResDto in guard let self = self else { return } appState[\.userSession] = .authenticated - KeyChainManager.addItem(key: .accessToken, value: signUpResDto.accessToken) + appState[\.routing.onboarding.signUp] = false + KeyChainManager.addItem(key: .accessToken, value: signUpResDto.accessToken) } ) .store(in: cancleBag) @@ -149,26 +152,6 @@ final class UserDataMigrationInteractor: UserDataInteractor { .store(in: cancleBag) } - /// 로그인된 유저정보를 가져옵니다. - func getUser() { - authRepository - .getUser() - .receive(on: DispatchQueue.main) - .sink( - receiveCompletion: { [weak self] completion in - guard let self = self else { return } - switch completion { - case .finished: - appState[\.userSession] = .authenticated - case .failure(_): - appState[\.userSession] = .unauthenticated - } - }, - receiveValue: { _ in } - ) - .store(in: cancleBag) - } - /// 유저인증 상태를 체크합니다. func checkAuthentication() { authRepository @@ -184,6 +167,7 @@ final class UserDataMigrationInteractor: UserDataInteractor { } }, receiveValue: { [weak self] tokenValidRes in guard let self = self else { return } + if tokenValidRes.role == "USER" { appState[\.userSession] = .authenticated } else { @@ -193,12 +177,16 @@ final class UserDataMigrationInteractor: UserDataInteractor { .store(in: cancleBag) } - func getUserProfile(userProfile: Binding) { + func getUserProfile() { authRepository .getUserProfile() .receive(on: DispatchQueue.main) - .sink(receiveCompletion: { _ in }) { - userProfile.wrappedValue = $0 + .sink(receiveCompletion: { _ in }) { [weak self] userProfile in + guard let self = self else { return } + appState.bulkUpdate { appState in + appState.userData.nickname = userProfile.nickname + appState.userData.department = userProfile.department + } } .store(in: cancleBag) } diff --git a/HongikYeolgong2/Domain/Interactors/UserDataInteractor.swift b/HongikYeolgong2/Domain/Interactors/UserDataInteractor.swift index 46e527f..72c2a82 100644 --- a/HongikYeolgong2/Domain/Interactors/UserDataInteractor.swift +++ b/HongikYeolgong2/Domain/Interactors/UserDataInteractor.swift @@ -14,10 +14,9 @@ protocol UserDataInteractor: AnyObject { func requestAppleLogin(_ authorization: ASAuthorization) func signUp(nickname: String, department: Department) func logout() - func getUser() func checkAuthentication() func checkUserNickname(nickname: String, nicknameCheckSubject: CurrentValueSubject) - func getUserProfile(userProfile: Binding) + func getUserProfile() func withdraw() } @@ -108,26 +107,6 @@ final class UserDataInteractorImpl: UserDataInteractor { .store(in: cancleBag) } - /// 로그인된 유저정보를 가져옵니다. - func getUser() { - authRepository - .getUser() - .receive(on: DispatchQueue.main) - .sink( - receiveCompletion: { [weak self] completion in - guard let self = self else { return } - switch completion { - case .finished: - appState[\.userSession] = .authenticated - case .failure(_): - appState[\.userSession] = .unauthenticated - } - }, - receiveValue: { _ in } - ) - .store(in: cancleBag) - } - /// 유저인증 상태를 체크합니다. func checkAuthentication() { authRepository @@ -152,12 +131,12 @@ final class UserDataInteractorImpl: UserDataInteractor { .store(in: cancleBag) } - func getUserProfile(userProfile: Binding) { + func getUserProfile() { authRepository .getUserProfile() .receive(on: DispatchQueue.main) - .sink(receiveCompletion: { _ in }) { - userProfile.wrappedValue = $0 + .sink(receiveCompletion: { _ in }) { _ in + } .store(in: cancleBag) } diff --git a/HongikYeolgong2/Domain/Interactors/UserPermissionsInteractor.swift b/HongikYeolgong2/Domain/Interactors/UserPermissionsInteractor.swift index 1af238b..d36ca2a 100644 --- a/HongikYeolgong2/Domain/Interactors/UserPermissionsInteractor.swift +++ b/HongikYeolgong2/Domain/Interactors/UserPermissionsInteractor.swift @@ -31,6 +31,7 @@ extension Permission { protocol UserPermissionsInteractor: AnyObject { func resolveStatus(for permission: Permission) func request(permission: Permission) + func handleNotificationPermissions() } final class RealUserPermissionsInteractor: UserPermissionsInteractor { @@ -42,12 +43,19 @@ final class RealUserPermissionsInteractor: UserPermissionsInteractor { self.openAppSetting = openAppSetting } + func handleNotificationPermissions() { + if appState.value.permissions.push == .granted { + appState[\.userData.isOnAlarm].toggle() + } else { + request(permission: .localNotifications) + } + } + /// 특정 권한 상태를 확인하고 앱 상태를 업데이트 합니다. /// - Parameter permission: 확인할 권한 종류 func resolveStatus(for permission: Permission) { let keyPath = AppState.permissionKeyPath(for: .localNotifications) let currentStatus = appState[keyPath] - guard currentStatus == .unknown else { return } let onResolve: (Permission.Status) -> Void = { [weak appState] status in appState?[keyPath] = status } @@ -102,6 +110,7 @@ private extension RealUserPermissionsInteractor { center.requestAuthorization(options: [.alert, .sound]) { (isGranted, error) in DispatchQueue.main.async { self.appState[\.permissions.push] = isGranted ? .granted : .denied + self.appState[\.userData.isOnAlarm] = isGranted ? true : false } } } diff --git a/HongikYeolgong2/Injected/AppState.swift b/HongikYeolgong2/Injected/AppState.swift index 0306d2f..e50e677 100644 --- a/HongikYeolgong2/Injected/AppState.swift +++ b/HongikYeolgong2/Injected/AppState.swift @@ -28,7 +28,12 @@ extension AppState { extension AppState { /// 앱 전역에서 사용하는 유저데이터 입니다. struct UserData: Equatable { - var isLoggedIn = false + var nickname = "" + var department = "" + var isOnAlarm: Bool { + get { UserDefaults.standard.bool(forKey: "isOnAlarm") } + set { UserDefaults.standard.set(newValue, forKey: "isOnAlarm") } + } } } diff --git a/HongikYeolgong2/Models/Page.swift b/HongikYeolgong2/Models/Page.swift new file mode 100644 index 0000000..e2e3808 --- /dev/null +++ b/HongikYeolgong2/Models/Page.swift @@ -0,0 +1,13 @@ +// +// Page.swift +// HongikYeolgong2 +// +// Created by 권석기 on 11/14/24. +// + +import Foundation + +enum Page: Hashable { + case webView(title: String, url: String) + case signUp +} diff --git a/HongikYeolgong2/Presentation/Auth/Onboarding/Component/OnboardingPageView.swift b/HongikYeolgong2/Presentation/Auth/Onboarding/Component/OnboardingPageView.swift index e4e2760..75ad23a 100644 --- a/HongikYeolgong2/Presentation/Auth/Onboarding/Component/OnboardingPageView.swift +++ b/HongikYeolgong2/Presentation/Auth/Onboarding/Component/OnboardingPageView.swift @@ -8,7 +8,7 @@ import SwiftUI struct OnboardingPageView: View { - @Binding var tabIndex: Int + @State private var tabIndex = 0 var body: some View { VStack { @@ -42,5 +42,5 @@ struct OnboardingPageView: View { } #Preview { - OnboardingPageView(tabIndex: .constant(0)) + OnboardingPageView() } diff --git a/HongikYeolgong2/Presentation/Auth/Onboarding/OnboardingView.swift b/HongikYeolgong2/Presentation/Auth/Onboarding/OnboardingView.swift index ff513b3..658acbc 100644 --- a/HongikYeolgong2/Presentation/Auth/Onboarding/OnboardingView.swift +++ b/HongikYeolgong2/Presentation/Auth/Onboarding/OnboardingView.swift @@ -1,43 +1,51 @@ +// +// OnboardingView.swift +// HongikYeolgong2 +// +// Created by 권석기 on 11/14/24. +// + import SwiftUI import Combine import AuthenticationServices struct OnboardingView: View { // MARK: - Properties - @Environment(\.injected) private var injected: DIContainer + @Environment(\.injected.appState) private var appState + @Environment(\.injected.interactors.userDataInteractor) var userDataInteractor // MARK: - States - @State private var tabIndex = 0 + @State private var onboardingPath: [Page] = [] @State private var routingState: Routing = .init() private var routingBinding: Binding { - $routingState.dispatched(to: injected.appState, \.routing.onboarding) + $routingState.dispatched(to: appState, \.routing.onboarding) } // MARK: - Body var body: some View { - NavigationStack { + NavigationStack(path: $onboardingPath) { VStack { Spacer() - OnboardingPageView(tabIndex: $tabIndex) + OnboardingPageView() AppleLoginButton( onRequest: onRequestAppleLogin, onCompletion: onCompleteAppleLogin ) - - NavigationLink( - destination: SignUpView(), - isActive: routingBinding.signUp - ) { + } + .onReceive(routingUpdate) { + onboardingPath.append(.signUp) + } + .navigationDestination(for: Page.self) { page in + switch page { + case .signUp: + SignUpView() + default: EmptyView() } - .opacity(0) - .frame(width: 0, height: 0) } - .onReceive(routingUpdate) { routingState = $0 } } - .navigationViewStyle(StackNavigationViewStyle()) } } @@ -50,8 +58,7 @@ private extension OnboardingView { func onCompleteAppleLogin(_ result: Result) { switch result { case let .success(authorization): - injected.interactors.userDataInteractor - .requestAppleLogin(authorization) + userDataInteractor.requestAppleLogin(authorization) case .failure: break } @@ -60,8 +67,11 @@ private extension OnboardingView { // MARK: - Routing private extension OnboardingView { - private var routingUpdate: AnyPublisher { - injected.appState.updates(for: \.routing.onboarding) + private var routingUpdate: AnyPublisher { + appState.updates(for: \.routing.onboarding.signUp) + .filter { $0 } + .map { _ in } + .eraseToAnyPublisher() } } diff --git a/HongikYeolgong2/Presentation/Auth/SignUp/SignUpView.swift b/HongikYeolgong2/Presentation/Auth/SignUp/SignUpView.swift index df0b995..3aa9c60 100644 --- a/HongikYeolgong2/Presentation/Auth/SignUp/SignUpView.swift +++ b/HongikYeolgong2/Presentation/Auth/SignUp/SignUpView.swift @@ -20,12 +20,7 @@ struct SignUpView: View { @State private var isSubmitButtonEnable = false @State private var isCheckButtonEnable = false @State private var keyboardOffset: CGFloat = 23 - let nicknameCheckSubject = CurrentValueSubject(false) - - // MARK: - Initialization - init() { -// UINavigationBar.setAnimationsEnabled(false) - } + let nicknameCheckSubject = CurrentValueSubject(false) // MARK: - Body var body: some View { @@ -88,13 +83,12 @@ struct SignUpView: View { .padding(.horizontal, 32.adjustToScreenWidth) } .toolbar(.hidden, for: .navigationBar) - .onChange(of: userInfo.inputNickname) { inputNickname in userInfo.nickname.validateUserNickname(nickname: inputNickname) } .onChange(of: userInfo) { userInfo in isSubmitButtonEnable = userInfo.nickname == .available && - userInfo.department != .none + userInfo.department != .none } .onReceive(nicknameCheckSubject.dropFirst()) { isAlreadyInUse in userInfo.nickname = isAlreadyInUse ? .alreadyUse : .available diff --git a/HongikYeolgong2/Presentation/Other/WebView.swift b/HongikYeolgong2/Presentation/Other/WebView.swift index 7d17bb5..e9a6607 100644 --- a/HongikYeolgong2/Presentation/Other/WebView.swift +++ b/HongikYeolgong2/Presentation/Other/WebView.swift @@ -47,6 +47,7 @@ struct WebViewWithNavigation: View { WebView(url: url) } .toolbar(.hidden, for: .navigationBar) + .edgesIgnoringSafeArea(.bottom) } } diff --git a/HongikYeolgong2/Presentation/Ranking/Component/RankingCell.swift b/HongikYeolgong2/Presentation/Ranking/Component/RankingCell.swift index 080f5cb..06c7942 100644 --- a/HongikYeolgong2/Presentation/Ranking/Component/RankingCell.swift +++ b/HongikYeolgong2/Presentation/Ranking/Component/RankingCell.swift @@ -90,6 +90,8 @@ extension RankingCell { private var rankImage: some View { switch (departmentRankInfo.rankChange, departmentRankInfo.currentRank) { + case let (rankChange, currentRank) where rankChange < 0 && currentRank == 2: + Image(.rankDownSecond) case let (rankChange, currentRank) where rankChange > 0 && currentRank == 1: Image(.rankUpGray) case let (rankChange, _) where rankChange > 0: diff --git a/HongikYeolgong2/Presentation/Root/InitialView.swift b/HongikYeolgong2/Presentation/Root/InitialView.swift index 1ef454a..fc16452 100644 --- a/HongikYeolgong2/Presentation/Root/InitialView.swift +++ b/HongikYeolgong2/Presentation/Root/InitialView.swift @@ -22,13 +22,18 @@ struct InitialView: View { OnboardingView() case .authenticated: MainTabView() + .onAppear { + userDataInteractor.getUserProfile() + } case .pending: SplashView() .ignoresSafeArea(.all) - .onAppear { checkUserSession() } + .onAppear { + checkUserSession() + } } } - .onAppear { + .onAppear { resolveUserPermissions() } .onReceive(canRequestFirstPushPermissions) { _ in diff --git a/HongikYeolgong2/Presentation/Setting/Component/MenuItem.swift b/HongikYeolgong2/Presentation/Setting/Component/MenuItem.swift new file mode 100644 index 0000000..21e4dba --- /dev/null +++ b/HongikYeolgong2/Presentation/Setting/Component/MenuItem.swift @@ -0,0 +1,40 @@ +// +// MenuItem.swift +// HongikYeolgong2 +// +// Created by 권석기 on 11/13/24. +// + +import SwiftUI + +struct MenuItem: View { + let title: String + var onTap: (() -> ())? + let content: (() -> Content)? + + var body: some View { + VStack { + HStack { + Text(title) + .font(.pretendard(size: 16, weight: .regular)) + .foregroundStyle(Color.gray200) + + Spacer() + + content?() + } + .padding(.horizontal, 16.adjustToScreenWidth) + .padding(.vertical, 13.adjustToScreenWidth) + } + .frame(height: 52.adjustToScreenHeight) + .background(Color.gray800) + .cornerRadius(8) + .onTapGesture { + onTap?() + } + } +} + +#Preview { + MenuItem(title: "", onTap: {}, content: { EmptyView() }) +} diff --git a/HongikYeolgong2/Presentation/Setting/SettingView.swift b/HongikYeolgong2/Presentation/Setting/SettingView.swift index 542e2c4..ae6790b 100644 --- a/HongikYeolgong2/Presentation/Setting/SettingView.swift +++ b/HongikYeolgong2/Presentation/Setting/SettingView.swift @@ -1,209 +1,182 @@ +// +// SettingView.swift +// HongikYeolgong2 +// +// Created by 권석기 on 11/13/24. +// import SwiftUI +import Combine struct SettingView: View { - @State private var isOnAlarm = true - @Environment(\.injected) var injected: DIContainer + @Environment(\.injected.appState) var appState @Environment(\.injected.interactors.userDataInteractor) var userDataInteractor - @State private var userProfile = UserProfile() - + @Environment(\.injected.interactors.userPermissionsInteractor) var userPermissionsInteractor + @State private var userProfile: AppState.UserData = .init() + @State private var isOnAlarm = UserDefaults.standard.bool(forKey: "isOnAlarm") + @State private var settingPath: [Page] = [] @State private var shouldShowWithdrawModal = false @State private var shouldShowLogoutModal = false - @State private var shouldShowNotice = false - @State private var shouldShowQna = false - var body: some View { - VStack(alignment:.leading, spacing: 0){ - NavigationLink("", - destination: WebViewWithNavigation(url: SecretKeys.noticeUrl, title: "공지사항") - .edgesIgnoringSafeArea(.bottom), - isActive: $shouldShowNotice) - .frame(width: 0, height: 0) - - NavigationLink("", - destination: WebViewWithNavigation(url: SecretKeys.qnaUrl, title: "문의사항") - .edgesIgnoringSafeArea(.bottom), - isActive: $shouldShowQna) - .frame(width: 0, height: 0) - + var body: some View { + NavigationStack(path: $settingPath) { VStack(alignment: .leading, spacing: 0) { - HStack(spacing: 0) { - Image(.settingIcon) - .resizable() - .frame(width: 55.adjustToScreenWidth, height: 55.adjustToScreenHeight) - .padding(.trailing, 19) + VStack(alignment: .leading, spacing: 0) { + HStack(spacing: 19.adjustToScreenWidth) { + ProfileImage() + + HStack(spacing: 8.adjustToScreenWidth) { + ProfileText(text: userProfile.nickname) + + + ProfileText(text: "|", textColor: Color.gray400) + + + ProfileText(text: userProfile.department) + } + } + + VStack(spacing: 20.adjustToScreenHeight) { + MenuItem(title: "공지사항") { + settingPath.append(.webView(title: "공지사항", url: SecretKeys.noticeUrl)) + } content: { + Image(.arrowRight) + } + + MenuItem(title: "문의사항") { + settingPath.append(.webView(title: "문의사항", url: SecretKeys.qnaUrl)) + } content: { + Image(.arrowRight) + } - Text(userProfile.nickname) - .font(.pretendard(size: 16, weight: .regular), lineHeight: 26.adjustToScreenHeight) - .padding(.trailing, 8) - .foregroundStyle(.gray200) - Text("|") - .font(.pretendard(size: 16, weight: .regular), lineHeight: 26.adjustToScreenHeight) - .padding(.trailing, 8) - .foregroundStyle(.gray300) - Text(userProfile.department) - .font(.pretendard(size: 16, weight: .regular), lineHeight: 26.adjustToScreenHeight) - .foregroundStyle(.gray200) + MenuItem(title: "열람실 종료 시간 알림") {} + content: { + Toggle("", isOn: Binding( + get: { isOnAlarm }, + set: { _ in userPermissionsInteractor.handleNotificationPermissions() } + )) + .toggleStyle(SwitchToggleStyle(tint: Color.blue100)) + .fixedSize() + .padding(.trailing, 3) + } + } + .padding(.top, 20.adjustToScreenHeight) + + InfomationView() + .padding(.top, 10.adjustToScreenHeight) } .padding(.top, 32.adjustToScreenHeight) - .padding(.bottom, 20.adjustToScreenHeight) - - Button(action: { - shouldShowNotice.toggle() - } - , label: { - Text("공지사항") - .font(.pretendard(size: 16, weight: .regular)) - .foregroundStyle(Color.gray200) - .minimumScaleFactor(0.2) - .frame(maxWidth: .infinity, - minHeight: 52.adjustToScreenHeight, alignment: .leading) - .padding(.leading, 16.adjustToScreenWidth) - - Image(.arrowRight) - .padding(.trailing, 11) - }) - .background(Color.gray800) - .cornerRadius(8) - .padding(.bottom, 20.adjustToScreenHeight) - Button(action: { - shouldShowQna.toggle() - } - , label: { - Text("문의사항") - .font(.pretendard(size: 16, weight: .regular)) - .foregroundStyle(Color.gray200) - .minimumScaleFactor(0.2) - .frame(maxWidth: .infinity, - minHeight: 52.adjustToScreenHeight, alignment: .leading) - .padding(.leading, 16.adjustToScreenWidth) - - Image(.arrowRight) - .padding(.trailing, 11) - }) - .background(Color.gray800) - .cornerRadius(8) - .padding(.bottom, 20.adjustToScreenHeight) + Spacer() HStack(spacing: 0) { - Text("열람실 종료 시간 알림") - .font(.pretendard(size: 16, weight: .regular)) - .foregroundStyle(Color.gray200) - .minimumScaleFactor(0.2) - .frame(maxWidth: .infinity, - minHeight: 52.adjustToScreenHeight, alignment: .leading) - .padding(.leading, 16.adjustToScreenWidth) + Spacer() + Button { + shouldShowLogoutModal.toggle() + } label: { + ProfileText(text: "로그아웃", textColor: Color.gray300) + } - Toggle("", isOn: Binding( - get: { isOnAlarm }, - set: { - isOnAlarm = $0 - } - )) - .toggleStyle(ColoredToggleStyle(onColor:Color.blue100)) - } - .background(Color.gray800) - .cornerRadius(8) - .padding(.bottom, 10.adjustToScreenHeight) - - HStack(spacing: 0) { - Image(.icInformation) - .padding(.trailing, 6.adjustToScreenWidth) - .foregroundStyle(.gray300) - Text("열람실 종료 10분, 30분 전에 알림을 보내 연장을 돕습니다.") - .font(.pretendard(size: 12, weight: .regular), lineHeight: 18.adjustToScreenHeight) - .foregroundStyle(.gray300) + ProfileText(text: "|", textColor: Color.gray400) + .padding(.horizontal, 24.adjustToScreenWidth) + + Button { + shouldShowWithdrawModal.toggle() + } label: { + ProfileText(text: "회원탈퇴", textColor: Color.gray300) + } + Spacer() } + .padding(.bottom, 36.adjustToScreenHeight) } - - Spacer() - - HStack(alignment: .center, spacing: 0) { - Spacer() - Button(action: { - shouldShowLogoutModal.toggle() - }, label: { - Text("로그아웃") - .font(.pretendard(size: 16, weight: .regular), lineHeight: 26.adjustToScreenHeight) - .foregroundStyle(Color.gray300) - }) - - Text("|") - .font(.pretendard(size: 16, weight: .regular), lineHeight: 26.adjustToScreenHeight) - .foregroundStyle(Color.gray300) - .padding(.horizontal, 24.adjustToScreenWidth) - - Button(action: { - shouldShowWithdrawModal.toggle() - }, label: { - Text("회원탈퇴") - .font(.pretendard(size: 16, weight: .regular), lineHeight: 26.adjustToScreenHeight) - .foregroundStyle(Color.gray300) - }) - Spacer() + .padding(.horizontal, 32.adjustToScreenWidth) + .modifier(IOSBackground()) + .systemOverlay(isPresented: $shouldShowWithdrawModal) { + ModalView(isPresented: $shouldShowWithdrawModal, + title: "정말 탈퇴하실 건가요?", + confirmButtonText: "돌아가기", + cancleButtonText: "탈퇴하기", + confirmAction: {}, + cancleAction: { userDataInteractor.withdraw() }) + } + .systemOverlay(isPresented: $shouldShowLogoutModal) { + ModalView(isPresented: $shouldShowLogoutModal, + title: "로그아웃 하실 건가요", + confirmButtonText: "돌아가기", + cancleButtonText: "로그아웃하기", + confirmAction: {}, + cancleAction: { userDataInteractor.logout() }) + } + .onReceive(isOnAlarmUpdated) { + isOnAlarm = $0 + } + .onReceive(isSceneActive) { + userPermissionsInteractor.resolveStatus(for: .localNotifications) + } + .onReceive(userProfileUpdated) { + userProfile = $0 + } + .navigationDestination(for: Page.self) { page in + switch page { + case let .webView(title, url): + WebViewWithNavigation(url: url, title: title) + default: + EmptyView() + } } - .padding(.bottom, 32.adjustToScreenHeight) - } - .systemOverlay(isPresented: $shouldShowWithdrawModal) { - ModalView(isPresented: $shouldShowWithdrawModal, - title: "정말 탈퇴하실 건가요?", - confirmButtonText: "돌아가기", - cancleButtonText: "탈퇴하기", - confirmAction: {}, - cancleAction: { userDataInteractor.withdraw() } - ) - } - .systemOverlay(isPresented: $shouldShowLogoutModal) { - ModalView(isPresented: $shouldShowLogoutModal, - title: "로그아웃 하실 건가요", - confirmButtonText: "돌아가기", - cancleButtonText: "로그아웃하기", - confirmAction: {}, - cancleAction: { injected.interactors.userDataInteractor.logout() } - ) - } - .padding(.horizontal, 32.adjustToScreenWidth) - .modifier(IOSBackground()) - .onAppear { - userDataInteractor.getUserProfile(userProfile: $userProfile) } } } -struct ColoredToggleStyle: ToggleStyle { - var label = "" - var onColor = Color.green - var offColor = Color.gray200 - var thumbColor = Color.white +// MARK: - Publishers +extension SettingView { + var isOnAlarmUpdated: AnyPublisher { + appState.updates(for: \.userData.isOnAlarm) + } - func makeBody(configuration: Self.Configuration) -> some View { - HStack { - Text(label) - Spacer() - Button(action: { configuration.isOn.toggle() } ) - { - RoundedRectangle(cornerRadius: 16, style: .circular) - .fill(configuration.isOn ? onColor : offColor) - .frame(width: 50, height: 29) - .overlay( - Circle() - .fill(thumbColor) - .shadow(radius: 1, x: 0, y: 1) - .padding(1.5) - .offset(x: configuration.isOn ? 10 : -10)) - .onChange(of: configuration.isOn) { _ in - withAnimation(.easeInOut(duration: 0.25)) { - } - } - } - } - .font(.title) - .padding(.horizontal) + var isSceneActive: AnyPublisher { + appState.updates(for: \.system.scenePhase) + .filter { $0 == .active } + .map { _ in } + .eraseToAnyPublisher() + } + + var userProfileUpdated: AnyPublisher { + appState.updates(for: \.userData) + } +} + +// MARK: - SubViews +struct ProfileText: View { + let text: String + var textColor: Color = .gray200 + + var body: some View { + Text(text) + .font(.pretendard(size: 16, weight: .regular), lineHeight: 26.adjustToScreenHeight) + .foregroundStyle(textColor) + } +} + +struct ProfileImage: View { + var body: some View { + Image(.settingIcon) + .resizable() + .frame(width: 55.adjustToScreenWidth, height: 55.adjustToScreenHeight) } } -//#Preview { -// SettingView() -//} +struct InfomationView: View { + var body: some View { + HStack(spacing: 6.adjustToScreenWidth) { + Image(.icInformation) + .foregroundStyle(.gray300) + + Text("열람실 종료 10분, 30분 전에 알림을 보내 연장을 돕습니다.") + .font(.pretendard(size: 12, weight: .regular), + lineHeight: 18.adjustToScreenHeight) + .foregroundStyle(.gray300) + } + } +} diff --git a/HongikYeolgong2/Resources/Assets.xcassets/Image/rankDownSecond.imageset/Contents.json b/HongikYeolgong2/Resources/Assets.xcassets/Image/rankDownSecond.imageset/Contents.json new file mode 100644 index 0000000..ef2cb53 --- /dev/null +++ b/HongikYeolgong2/Resources/Assets.xcassets/Image/rankDownSecond.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "rankDownSecond.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "down_2nd@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "down_2nd@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/HongikYeolgong2/Resources/Assets.xcassets/Image/rankDownSecond.imageset/down_2nd@2x.png b/HongikYeolgong2/Resources/Assets.xcassets/Image/rankDownSecond.imageset/down_2nd@2x.png new file mode 100644 index 0000000..acb00e9 Binary files /dev/null and b/HongikYeolgong2/Resources/Assets.xcassets/Image/rankDownSecond.imageset/down_2nd@2x.png differ diff --git a/HongikYeolgong2/Resources/Assets.xcassets/Image/rankDownSecond.imageset/down_2nd@3x.png b/HongikYeolgong2/Resources/Assets.xcassets/Image/rankDownSecond.imageset/down_2nd@3x.png new file mode 100644 index 0000000..d4f99fb Binary files /dev/null and b/HongikYeolgong2/Resources/Assets.xcassets/Image/rankDownSecond.imageset/down_2nd@3x.png differ diff --git a/HongikYeolgong2/Resources/Assets.xcassets/Image/rankDownSecond.imageset/rankDownSecond.png b/HongikYeolgong2/Resources/Assets.xcassets/Image/rankDownSecond.imageset/rankDownSecond.png new file mode 100644 index 0000000..b665405 Binary files /dev/null and b/HongikYeolgong2/Resources/Assets.xcassets/Image/rankDownSecond.imageset/rankDownSecond.png differ diff --git a/HongikYeolgong2/Util/API/Base/NetworkService.swift b/HongikYeolgong2/Util/API/Base/NetworkService.swift index 8532589..15e7f3a 100644 --- a/HongikYeolgong2/Util/API/Base/NetworkService.swift +++ b/HongikYeolgong2/Util/API/Base/NetworkService.swift @@ -90,7 +90,6 @@ extension NetworkService { return try decoder.decode(T.self, from: data) } catch { - print(error) throw NetworkError.decodingError(error.localizedDescription) } } diff --git a/HongikYeolgong2/Util/Services/AppleLoginManager.swift b/HongikYeolgong2/Util/Services/AppleLoginManager.swift index 5adc838..eb55212 100644 --- a/HongikYeolgong2/Util/Services/AppleLoginManager.swift +++ b/HongikYeolgong2/Util/Services/AppleLoginManager.swift @@ -28,7 +28,7 @@ final class AppleLoginManager: NSObject, AppleLoginService, ASAuthorizationContr guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential else { return nil } - + return appleIDCredential }