diff --git a/Frontend-iOS/FebirdApp/FebirdApp.xcodeproj/project.pbxproj b/Frontend-iOS/FebirdApp/FebirdApp.xcodeproj/project.pbxproj index c0c05fe8..22f01aca 100644 --- a/Frontend-iOS/FebirdApp/FebirdApp.xcodeproj/project.pbxproj +++ b/Frontend-iOS/FebirdApp/FebirdApp.xcodeproj/project.pbxproj @@ -7,8 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 323408F52C61B3CE00FCB9B6 /* SocialLoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 323408F42C61B3CE00FCB9B6 /* SocialLoginViewModel.swift */; }; - 323408F72C61C91D00FCB9B6 /* SignInWithAppleViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 323408F62C61C91D00FCB9B6 /* SignInWithAppleViewModel.swift */; }; 323408FB2C61C97500FCB9B6 /* SignInWithAppleButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 323408FA2C61C97500FCB9B6 /* SignInWithAppleButton.swift */; }; 3E3B9A812C5B29A2001F2852 /* NavigationPathFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E3B9A802C5B29A2001F2852 /* NavigationPathFinder.swift */; }; 3E3B9A832C5B29AA001F2852 /* ViewOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E3B9A822C5B29AA001F2852 /* ViewOptions.swift */; }; @@ -59,9 +57,10 @@ 8F0F48062C63723F004E3B86 /* RoutineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F0F48052C63723F004E3B86 /* RoutineViewModel.swift */; }; 8F0F48082C637261004E3B86 /* LevelViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F0F48072C637261004E3B86 /* LevelViewModel.swift */; }; 8F0F480A2C6372EA004E3B86 /* ExerciseViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F0F48092C6372EA004E3B86 /* ExerciseViewModel.swift */; }; - 8F0F48132C637905004E3B86 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 8F0F48122C637905004E3B86 /* Alamofire */; }; 8F0F48152C63795A004E3B86 /* HistoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F0F48142C63795A004E3B86 /* HistoryViewModel.swift */; }; 8F0F48172C637985004E3B86 /* MemberViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F0F48162C637985004E3B86 /* MemberViewModel.swift */; }; + 8F1409232C6DFDC6003DD085 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F1409222C6DFDC6003DD085 /* MainView.swift */; }; + 8F60F9D62C6DEC3D0022A733 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 8F60F9D52C6DEC3D0022A733 /* Alamofire */; }; 8F8A77E72C5E7AF9008E61D7 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F8A77E62C5E7AF9008E61D7 /* SettingsViewModel.swift */; }; 8F8A77EA2C5E7CE1008E61D7 /* CellConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F8A77E92C5E7CE1008E61D7 /* CellConfiguration.swift */; }; 8F8A77EC2C5F554F008E61D7 /* UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F8A77EB2C5F554F008E61D7 /* UserProfile.swift */; }; @@ -178,8 +177,6 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 323408F42C61B3CE00FCB9B6 /* SocialLoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialLoginViewModel.swift; sourceTree = ""; }; - 323408F62C61C91D00FCB9B6 /* SignInWithAppleViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInWithAppleViewModel.swift; sourceTree = ""; }; 323408FA2C61C97500FCB9B6 /* SignInWithAppleButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInWithAppleButton.swift; sourceTree = ""; }; 323408FC2C61CA3B00FCB9B6 /* FebirdApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FebirdApp.entitlements; sourceTree = ""; }; 3E3B9A802C5B29A2001F2852 /* NavigationPathFinder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationPathFinder.swift; sourceTree = ""; }; @@ -237,6 +234,7 @@ 8F0F48092C6372EA004E3B86 /* ExerciseViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExerciseViewModel.swift; sourceTree = ""; }; 8F0F48142C63795A004E3B86 /* HistoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryViewModel.swift; sourceTree = ""; }; 8F0F48162C637985004E3B86 /* MemberViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberViewModel.swift; sourceTree = ""; }; + 8F1409222C6DFDC6003DD085 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; 8F8A77E62C5E7AF9008E61D7 /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; 8F8A77E92C5E7CE1008E61D7 /* CellConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellConfiguration.swift; sourceTree = ""; }; 8F8A77EB2C5F554F008E61D7 /* UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfile.swift; sourceTree = ""; }; @@ -341,7 +339,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 8F0F48132C637905004E3B86 /* Alamofire in Frameworks */, + 8F60F9D62C6DEC3D0022A733 /* Alamofire in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -411,8 +409,6 @@ isa = PBXGroup; children = ( 3E3B9A8D2C5B500E001F2852 /* SocialLoginView.swift */, - 323408F42C61B3CE00FCB9B6 /* SocialLoginViewModel.swift */, - 323408F62C61C91D00FCB9B6 /* SignInWithAppleViewModel.swift */, 323408FA2C61C97500FCB9B6 /* SignInWithAppleButton.swift */, ); path = Login; @@ -530,6 +526,7 @@ isa = PBXGroup; children = ( 3EF6F04C2C36937E00EEF18F /* FebirdAppApp.swift */, + 8F1409222C6DFDC6003DD085 /* MainView.swift */, 8FE8FB342C60B9FA009CFAE9 /* NetworkService */, 3E3B9A8C2C5B4FEF001F2852 /* Login */, 3E9E9E0B2C44E5B1002389EB /* Extensions */, @@ -918,7 +915,7 @@ ); name = FebirdApp; packageProductDependencies = ( - 8F0F48122C637905004E3B86 /* Alamofire */, + 8F60F9D52C6DEC3D0022A733 /* Alamofire */, ); productName = FebirdApp; productReference = 3EF6F0492C36937E00EEF18F /* FebirdApp.app */; @@ -993,8 +990,8 @@ ); mainGroup = 3EF6F0402C36937D00EEF18F; packageReferences = ( - 8F0F48102C6375A9004E3B86 /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */, - 8F0F48112C637905004E3B86 /* XCRemoteSwiftPackageReference "Alamofire" */, + 8F60F9D32C6DEC320022A733 /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */, + 8F60F9D42C6DEC3D0022A733 /* XCRemoteSwiftPackageReference "Alamofire" */, ); productRefGroup = 3EF6F04A2C36937E00EEF18F /* Products */; projectDirPath = ""; @@ -1078,6 +1075,7 @@ buildActionMask = 2147483647; files = ( 8F0F47FA2C636FA0004E3B86 /* Inbody.swift in Sources */, + 8F1409232C6DFDC6003DD085 /* MainView.swift in Sources */, E20F9CF62C63DB20006A034B /* CustomTextField.swift in Sources */, 8FE8FB362C60BA2E009CFAE9 /* NetworkManager.swift in Sources */, E20F9CF82C63DEA9006A034B /* ChatTextFieldView.swift in Sources */, @@ -1153,7 +1151,6 @@ 8F0F48172C637985004E3B86 /* MemberViewModel.swift in Sources */, 8F8A77E72C5E7AF9008E61D7 /* SettingsViewModel.swift in Sources */, E10FD9B02C4CC01700D80139 /* InbodyMainView.swift in Sources */, - 323408F52C61B3CE00FCB9B6 /* SocialLoginViewModel.swift in Sources */, 8F8A77EE2C5F56BD008E61D7 /* ProfileSettingViewModel.swift in Sources */, E2CFF20E2C4E999E00540500 /* MessageInUI.swift in Sources */, E15BF0302C609C300033E5C3 /* ExerciseDetector+ExerciseDetection.swift in Sources */, @@ -1167,7 +1164,6 @@ E10FD99F2C49FECB00D80139 /* OnboardingLoadingView.swift in Sources */, 8F0F47FE2C636FEC004E3B86 /* Level.swift in Sources */, 8F8A77F02C5F734B008E61D7 /* SettingHeaderView.swift in Sources */, - 323408F72C61C91D00FCB9B6 /* SignInWithAppleViewModel.swift in Sources */, E15BF02C2C609AF80033E5C3 /* ExerciseState.swift in Sources */, E1BB0D622C48A875002565B2 /* InbodyAddView.swift in Sources */, E2FDD2572C4B93AF004067EF /* WeeklyCalendar.swift in Sources */, @@ -1553,7 +1549,7 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 8F0F48102C6375A9004E3B86 /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */ = { + 8F60F9D32C6DEC320022A733 /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/SimplyDanny/SwiftLintPlugins.git"; requirement = { @@ -1561,7 +1557,7 @@ minimumVersion = 0.56.1; }; }; - 8F0F48112C637905004E3B86 /* XCRemoteSwiftPackageReference "Alamofire" */ = { + 8F60F9D42C6DEC3D0022A733 /* XCRemoteSwiftPackageReference "Alamofire" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Alamofire/Alamofire.git"; requirement = { @@ -1572,9 +1568,9 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 8F0F48122C637905004E3B86 /* Alamofire */ = { + 8F60F9D52C6DEC3D0022A733 /* Alamofire */ = { isa = XCSwiftPackageProductDependency; - package = 8F0F48112C637905004E3B86 /* XCRemoteSwiftPackageReference "Alamofire" */; + package = 8F60F9D42C6DEC3D0022A733 /* XCRemoteSwiftPackageReference "Alamofire" */; productName = Alamofire; }; /* End XCSwiftPackageProductDependency section */ diff --git a/Frontend-iOS/FebirdApp/FebirdApp/Resources/Info.plist b/Frontend-iOS/FebirdApp/FebirdApp/Resources/Info.plist index 0e7453e2..d5ff6b97 100644 --- a/Frontend-iOS/FebirdApp/FebirdApp/Resources/Info.plist +++ b/Frontend-iOS/FebirdApp/FebirdApp/Resources/Info.plist @@ -2,6 +2,8 @@ + NSFaceIDUsageDescription + API_KEY $(API_KEY) BASE_URL diff --git a/Frontend-iOS/FebirdApp/FebirdApp/Sources/FebirdAppApp.swift b/Frontend-iOS/FebirdApp/FebirdApp/Sources/FebirdAppApp.swift index 6b2a175e..cd9a2949 100644 --- a/Frontend-iOS/FebirdApp/FebirdApp/Sources/FebirdAppApp.swift +++ b/Frontend-iOS/FebirdApp/FebirdApp/Sources/FebirdAppApp.swift @@ -10,10 +10,9 @@ import SwiftData @main struct FebirdAppApp: App { - @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + // ViewModel 객체들 초기화 @StateObject private var tabViewModel = TabViewModel() @StateObject private var albumViewModel = AlbumViewModel() - @StateObject private var socialLoginViewModel = SocialLoginViewModel() @StateObject private var onboardingNavigationPathFinder = NavigationPathFinder() @StateObject private var mealNavigationPathFinder = NavigationPathFinder() @StateObject private var exerciseNavigationPathFinder = NavigationPathFinder() @@ -28,12 +27,14 @@ struct FebirdAppApp: App { @StateObject private var profileSelectViewModel = ProfileSelectViewModel() @StateObject private var profileSettingViewModel = ProfileSettingViewModel() @StateObject private var azureInbodyViewModel = AzureInbodyViewModel() + @StateObject private var authViewModel = AuthViewModel() let modelContainer: ModelContainer init() { do { - modelContainer = try ModelContainer(for: UserProfile.self, EyeBodyPhoto.self, LevelRecordData.self, DailyMemo.self, MealMemo.self) + // ModelContainer 초기화 + modelContainer = try ModelContainer(for: EyeBodyPhoto.self, LevelRecordData.self, DailyMemo.self, MealMemo.self) } catch { fatalError("Could not initialize ModelContainer: \(error)") } @@ -42,73 +43,33 @@ struct FebirdAppApp: App { var body: some Scene { WindowGroup { Group { - if socialLoginViewModel.loginResult == nil { + // 로그인 여부에 따라 화면을 분기 + if !authViewModel.loginResult { SocialLoginView() - } else if onboardingNavigationPathFinder.isFirstEnteredApp { - NavigationStack(path: $onboardingNavigationPathFinder.path) { - OnboardingWelcomView() - .navigationDestination(for: OnboardingViewOptions.self) { option in - option.view() - } - } } else { - ZStack(alignment: .bottom) { - switch tabViewModel.selectedTab { - case .meal: - NavigationStack(path: $mealNavigationPathFinder.path) { - MealMainView() - .navigationDestination(for: MealViewOptions.self) { option in - option.view() - } - } - case .exercise: - NavigationStack(path: $exerciseNavigationPathFinder.path) { - ExerciseMainView() - .navigationDestination(for: ExerciseViewOptions.self) { option in - option.view() - } - } - case .profile: - NavigationStack(path: $profileNavigationPathFinder.path) { - ProfileMainView() - .navigationDestination(for: ProfileViewOptions.self) { option in - option.view() - } - } - } - CustomTabBarView() - } - .environmentObject(tabViewModel) - .environmentObject(albumViewModel) - .environmentObject(mealNavigationPathFinder) - .environmentObject(exerciseNavigationPathFinder) - .environmentObject(profileNavigationPathFinder) - + // 메인 화면 + MainView() + .environmentObject(tabViewModel) + .environmentObject(albumViewModel) + .environmentObject(mealNavigationPathFinder) + .environmentObject(exerciseNavigationPathFinder) + .environmentObject(profileNavigationPathFinder) + .environmentObject(chatViewModel) + .environmentObject(historyViewModel) + .environmentObject(profileSelectViewModel) + .environmentObject(profileSettingViewModel) + .environmentObject(routineViewModel) + .environmentObject(levelViewModel) + .environmentObject(exerciseViewModel) + .environmentObject(inbodyViewModel) + .environmentObject(azureInbodyViewModel) + .environmentObject(memberViewModel) + .environmentObject(authViewModel) + .modelContainer(modelContainer) } } - .environmentObject(socialLoginViewModel) + .environmentObject(authViewModel) .environmentObject(onboardingNavigationPathFinder) - .environmentObject(chatViewModel) - .environmentObject(routineViewModel) - .environmentObject(levelViewModel) - .environmentObject(exerciseViewModel) - .environmentObject(inbodyViewModel) - .environmentObject(historyViewModel) - .environmentObject(profileSelectViewModel) - .environmentObject(profileSettingViewModel) - .environmentObject(azureInbodyViewModel) } - .environmentObject(memberViewModel) - .modelContainer(modelContainer) - } -} - -class AppDelegate: NSObject, UIApplicationDelegate { - func applicationDidEnterBackground(_ application: UIApplication) { - ChatViewModel.shared.clearMessages() - } - - func applicationWillTerminate(_ application: UIApplication) { - ChatViewModel.shared.clearMessages() } } diff --git a/Frontend-iOS/FebirdApp/FebirdApp/Sources/Login/SignInWithAppleViewModel.swift b/Frontend-iOS/FebirdApp/FebirdApp/Sources/Login/SignInWithAppleViewModel.swift deleted file mode 100644 index 70326350..00000000 --- a/Frontend-iOS/FebirdApp/FebirdApp/Sources/Login/SignInWithAppleViewModel.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// SignInWithAppleViewModel.swift -// FebirdApp -// -// Created by 김수영 on 8/6/24. -// - -import AuthenticationServices - -class SignInWithAppleViewModel: NSObject, ObservableObject, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding { - - @Published var userIdentifier: String = "" - @Published var userName: String = "" - @Published var userEmail: String = "" - - private var continueWithAuthorization: ((ASAuthorization) -> Void)? - private var continueWithError: ((Error) -> Void)? - - func startSignInWithAppleFlow() async throws { - let request = ASAuthorizationAppleIDProvider().createRequest() - request.requestedScopes = [.fullName, .email] - - let result = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - let authorizationController = ASAuthorizationController(authorizationRequests: [request]) - authorizationController.delegate = self - authorizationController.presentationContextProvider = self - authorizationController.performRequests() - - self.continueWithAuthorization = { authorization in - continuation.resume(returning: authorization) - } - self.continueWithError = { error in - continuation.resume(throwing: error) - } - } - } - - func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { - if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential { - userIdentifier = appleIDCredential.user - if let fullName = appleIDCredential.fullName { - userName = "\(fullName.givenName ?? "") \(fullName.familyName ?? "")" - } - userEmail = appleIDCredential.email ?? "" - } - continueWithAuthorization?(authorization) - } - - func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { - print("Authorization failed: \(error.localizedDescription)") - } - - func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { - let scenes = UIApplication.shared.connectedScenes - let windowScene = scenes.first as? UIWindowScene - let window = windowScene?.windows.first - return window! - } -} diff --git a/Frontend-iOS/FebirdApp/FebirdApp/Sources/Login/SocialLoginView.swift b/Frontend-iOS/FebirdApp/FebirdApp/Sources/Login/SocialLoginView.swift index 44f3f260..c54fb115 100644 --- a/Frontend-iOS/FebirdApp/FebirdApp/Sources/Login/SocialLoginView.swift +++ b/Frontend-iOS/FebirdApp/FebirdApp/Sources/Login/SocialLoginView.swift @@ -8,11 +8,12 @@ import SwiftUI struct SocialLoginView: View { - @EnvironmentObject private var socialLoginViewModel : SocialLoginViewModel - @EnvironmentObject private var memberViewModel : MemberViewModel - @StateObject private var appleLoginViewModel = SignInWithAppleViewModel() + @EnvironmentObject private var authViewModel: AuthViewModel @EnvironmentObject private var onboardingNavigationPathFinder: NavigationPathFinder + @State private var showingAlert = false + @State private var alertMessage = "" + var body: some View { ZStack { Color.orange50.ignoresSafeArea() @@ -52,16 +53,18 @@ struct SocialLoginView: View { .onTapGesture { Task { do { - let _: () = try await appleLoginViewModel.startSignInWithAppleFlow() - - if !appleLoginViewModel.userIdentifier.isEmpty { - try await socialLoginViewModel.loginWithApple(appleID: appleLoginViewModel.userIdentifier) + try await authViewModel.startSignInWithApple() - memberViewModel.newMember.appleID = appleLoginViewModel.userIdentifier - onboardingNavigationPathFinder.setIsFirstenteredApp(true) + if authViewModel.loginResult { + // 로그인 성공 후, 첫 진입 시 온보딩 화면으로 이동 + if onboardingNavigationPathFinder.isFirstEnteredApp { + onboardingNavigationPathFinder.addPath(option: .onboardingWelcome) + } else { + onboardingNavigationPathFinder.popToRoot() + } } } catch { - print("Error: \(error.localizedDescription)") + print("Apple 로그인 실패: \(error.localizedDescription)") } } } diff --git a/Frontend-iOS/FebirdApp/FebirdApp/Sources/Login/SocialLoginViewModel.swift b/Frontend-iOS/FebirdApp/FebirdApp/Sources/Login/SocialLoginViewModel.swift deleted file mode 100644 index ea718a67..00000000 --- a/Frontend-iOS/FebirdApp/FebirdApp/Sources/Login/SocialLoginViewModel.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// LoginViewModel.swift -// FebirdApp -// -// Created by 김수영 on 8/4/24. -// - -import Foundation -import SwiftUI -import Combine - -class SocialLoginViewModel: ObservableObject { - @Published var loginResult: String? - private var cancellables = Set() - - func loginWithApple(appleID: String) async throws { - let url = URL(string: "https://app-feo.azurewebsites.net/member/apple-login")! - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - - let body = ["appleID": appleID] - request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: []) - - print("Sending request to \(url) with body: \(body)") // 요청 정보 로깅 - - URLSession.shared.dataTaskPublisher(for: request) - .tryMap { result -> Data in - guard let httpResponse = result.response as? HTTPURLResponse else { - throw URLError(.badServerResponse) - } - print("Received HTTP response: \(httpResponse.statusCode)") // 응답 상태 코드 로깅 - if httpResponse.statusCode != 200 { - throw URLError(.badServerResponse) - } - return result.data - } - .decode(type: LoginResponse.self, decoder: JSONDecoder()) - .receive(on: DispatchQueue.main) - .sink(receiveCompletion: { completion in - if case .failure(let error) = completion { - print("Error: \(error.localizedDescription)") // 에러 메시지 로깅 - } - }, receiveValue: { [weak self] response in - print("Received response: \(response)") // 응답 데이터 로깅 - self?.loginResult = response.token - }) - .store(in: &cancellables) - } -} - -struct LoginResponse: Decodable { - let token: String -} diff --git a/Frontend-iOS/FebirdApp/FebirdApp/Sources/MainView.swift b/Frontend-iOS/FebirdApp/FebirdApp/Sources/MainView.swift new file mode 100644 index 00000000..94823805 --- /dev/null +++ b/Frontend-iOS/FebirdApp/FebirdApp/Sources/MainView.swift @@ -0,0 +1,44 @@ +// +// MainView.swift +// FebirdApp +// +// Created by DOYEON JEONG on 8/15/24. +// + +import SwiftUI + +struct MainView: View { + @EnvironmentObject private var tabViewModel: TabViewModel + @EnvironmentObject private var mealNavigationPathFinder: NavigationPathFinder + @EnvironmentObject private var exerciseNavigationPathFinder: NavigationPathFinder + @EnvironmentObject private var profileNavigationPathFinder: NavigationPathFinder + + var body: some View { + ZStack(alignment: .bottom) { + switch tabViewModel.selectedTab { + case .meal: + NavigationStack(path: $mealNavigationPathFinder.path) { + MealMainView() + .navigationDestination(for: MealViewOptions.self) { option in + option.view() + } + } + case .exercise: + NavigationStack(path: $exerciseNavigationPathFinder.path) { + ExerciseMainView() + .navigationDestination(for: ExerciseViewOptions.self) { option in + option.view() + } + } + case .profile: + NavigationStack(path: $profileNavigationPathFinder.path) { + ProfileMainView() + .navigationDestination(for: ProfileViewOptions.self) { option in + option.view() + } + } + } + CustomTabBarView() + } + } +} diff --git a/Frontend-iOS/FebirdApp/FebirdApp/Sources/NetworkService/AuthViewModel.swift b/Frontend-iOS/FebirdApp/FebirdApp/Sources/NetworkService/AuthViewModel.swift index ea78ea7b..eb7fb721 100644 --- a/Frontend-iOS/FebirdApp/FebirdApp/Sources/NetworkService/AuthViewModel.swift +++ b/Frontend-iOS/FebirdApp/FebirdApp/Sources/NetworkService/AuthViewModel.swift @@ -10,119 +10,100 @@ import Alamofire import AuthenticationServices import Combine -// 서버 응답을 디코딩하기 위한 구조체 -struct AuthResponse: Decodable { - let token: String -} - -// 인증 관련 비즈니스 로직을 처리하는 뷰 모델 -class AuthViewModel: NSObject, ObservableObject { - // 사용자 인증 상태를 나타내는 변수 (true: 인증됨, false: 인증되지 않음) - @Published var isAuthenticated = false - // 에러 발생 시 저장하는 변수 - @Published var error: Error? - - // Combine 프레임워크의 취소 가능한 객체들을 저장하는 집합 - private var cancellables = Set() - // API 기본 URL - private let baseURL: String - // API 키 - private let apiKey: String - - // 초기화 메서드 - override init() { - // Info.plist에서 API 관련 정보를 가져옴 - guard let baseURL = Bundle.main.infoDictionary?["API_BASE_URL"] as? String, - let apiKey = Bundle.main.infoDictionary?["API_KEY"] as? String else { - fatalError("Missing API configuration") - } - self.baseURL = baseURL - self.apiKey = apiKey - - // NSObject의 초기화 메서드 호출 - super.init() - - // 키체인에서 저장된 토큰을 불러옴 - if let data = KeychainManager.load(service: "AuthService", account: "UserToken"), - let token = String(data: data, encoding: .utf8) { - self.isAuthenticated = true - self.setAuthorizationHeader(token: token) - } - } - - // Apple 로그인 프로세스를 시작하는 메서드 - func signInWithApple() { - let appleIDProvider = ASAuthorizationAppleIDProvider() - let request = appleIDProvider.createRequest() - request.requestedScopes = [.fullName, .email] - - let authorizationController = ASAuthorizationController(authorizationRequests: [request]) - authorizationController.delegate = self - authorizationController.performRequests() +@MainActor +class AuthViewModel: NSObject, ObservableObject, ASAuthorizationControllerDelegate { + @Published var loginResult = false + private let keychainManager = KeychainManager.shared + private var signInContinuation: CheckedContinuation? + + // 1. Apple 로그인 요청 시작 + func startSignInWithApple() async throws { + let request = createAppleIDRequest() + let appleIDToken = try await performAppleSignIn(with: request) + try await sendAppleTokenToServer(appleIDToken: appleIDToken) } - - // 로그아웃 메서드 - func signOut() { - // 키체인에서 토큰 삭제 - KeychainManager.delete(service: "AuthService", account: "UserToken") - // 헤더에서 인증 정보 제거 - AF.session.configuration.headers["Authorization"] = nil - isAuthenticated = false + + // Apple ID 요청 생성 + private func createAppleIDRequest() -> ASAuthorizationAppleIDRequest { + let request = ASAuthorizationAppleIDProvider().createRequest() + request.requestedScopes = [.fullName, .email] // 사용자 이름과 이메일 요청 + return request } - - // 서버에 인증 요청을 보내는 메서드 - private func authenticateWithServer(idToken: String) async throws { - let url = "\(baseURL)/auth/apple" - let parameters: [String: String] = ["id_token": idToken] - - let task = AF.request(url, method: .post, parameters: parameters, headers: ["Authorization": "Bearer \(apiKey)"]) - .validate() - .serializingDecodable(AuthResponse.self) - let response = await task.response - - switch response.result { - case .success(let authResponse): - // 서버로부터 받은 토큰을 키체인에 저장 - if let data = authResponse.token.data(using: .utf8) { - KeychainManager.save(data, service: "AuthService", account: "UserToken") - } - setAuthorizationHeader(token: authResponse.token) - await MainActor.run { - self.isAuthenticated = true - } - case .failure(let error): - throw error + + // Apple Sign-In 실행 + private func performAppleSignIn(with request: ASAuthorizationAppleIDRequest) async throws -> String { + return try await withCheckedThrowingContinuation { continuation in + let authorizationController = ASAuthorizationController(authorizationRequests: [request]) + authorizationController.delegate = self + authorizationController.performRequests() + self.signInContinuation = continuation } } - - // 인증 헤더를 설정하는 메서드 - private func setAuthorizationHeader(token: String) { - AF.session.configuration.headers["Authorization"] = "Bearer \(token)" - } -} - -// Apple 로그인 결과를 처리하는 델리게이트 확장 -extension AuthViewModel: ASAuthorizationControllerDelegate { - // 인증이 성공적으로 완료되었을 때 호출되는 메서드 + + // 2. Apple 서버로부터 토큰 수신 func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential, - let idToken = appleIDCredential.identityToken, - let idTokenString = String(data: idToken, encoding: .utf8) { - - Task { - do { - try await authenticateWithServer(idToken: idTokenString) - } catch { - await MainActor.run { - self.error = error - } - } + let identityToken = appleIDCredential.identityToken, + let fullName = appleIDCredential.fullName, + let email = appleIDCredential.email { + } else { + signInContinuation?.resume(throwing: NSError(domain: "Apple SignIn", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to get Apple ID token"])) + } + } + + // Apple 로그인 오류 처리 + func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { + signInContinuation?.resume(throwing: error) + } + + // 3. Apple ID 토큰을 서버로 전송 + private func sendAppleTokenToServer(appleIDToken: String) async throws { + let url = "\(Config.baseURL)/member/apple-login" + let parameters: [String: Any] = ["appleID": appleIDToken] + + print("Sending request to URL: \(url)") + print("With parameters: \(parameters)") + + do { + let response = try await AF.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default) + .validate(statusCode: 200..<300) // 200-299 범위의 응답만 유효 + .serializingDecodable(LoginResponse.self).value + + handleLoginSuccess(with: response) + } catch let afError as AFError { + handleLoginError(afError) + + if let data = afError.underlyingError as? Data, let responseString = String(data: data, encoding: .utf8) { + print("AFError: \(afError.localizedDescription)") + print("Server response: \(responseString)") } + } catch { + handleLoginError(error) + print("Unexpected error: \(error.localizedDescription)") } } + + // 로그인 성공 처리 + private func handleLoginSuccess(with response: LoginResponse) { + self.loginResult = true + keychainManager.saveToken(response.token) + keychainManager.saveMemberID(response.memberId) + print("Login successful. Token: \(response.token), MemberID: \(response.memberId)") + } + + // 로그인 실패 처리 + private func handleLoginError(_ error: Error) { + self.loginResult = false + print("Login failed with error: \(error.localizedDescription)") + } +} - // 인증 과정에서 에러가 발생했을 때 호출되는 메서드 - func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { - self.error = error +struct LoginResponse: Decodable { + let token: String + let memberId: Int + + enum CodingKeys: String, CodingKey { + case token + case memberId = "member_id" } } diff --git a/Frontend-iOS/FebirdApp/FebirdApp/Sources/NetworkService/Chat/ChatViewModel.swift b/Frontend-iOS/FebirdApp/FebirdApp/Sources/NetworkService/Chat/ChatViewModel.swift index ca45a510..82d8e18f 100644 --- a/Frontend-iOS/FebirdApp/FebirdApp/Sources/NetworkService/Chat/ChatViewModel.swift +++ b/Frontend-iOS/FebirdApp/FebirdApp/Sources/NetworkService/Chat/ChatViewModel.swift @@ -34,11 +34,11 @@ class ChatViewModel: ObservableObject { ["role": "user", "content": content] ] ] - + let request = AF.request(url, method: .post, parameters: body, encoding: JSONEncoding.default, headers: headers) - + let response = await request.serializingDecodable(ChatResponse.self).response - + switch response.result { case .success(let chatResponse): if let message = chatResponse.choices.first?.message { diff --git a/Frontend-iOS/FebirdApp/FebirdApp/Sources/NetworkService/KeychainManager.swift b/Frontend-iOS/FebirdApp/FebirdApp/Sources/NetworkService/KeychainManager.swift index 77fdbb3e..d9608cf7 100644 --- a/Frontend-iOS/FebirdApp/FebirdApp/Sources/NetworkService/KeychainManager.swift +++ b/Frontend-iOS/FebirdApp/FebirdApp/Sources/NetworkService/KeychainManager.swift @@ -9,38 +9,70 @@ import Security import Foundation class KeychainManager { - static func save(_ data: Data, service: String, account: String) { - let query = [ - kSecValueData: data, - kSecAttrService: service, - kSecAttrAccount: account, - kSecClass: kSecClassGenericPassword - ] as CFDictionary + static let shared = KeychainManager() + private init() {} - SecItemAdd(query, nil) + func saveToken(_ token: String) { + guard let data = token.data(using: .utf8) else { return } + KeychainManager.save(data, service: "com.yourapp.token", account: "userToken") } - static func load(service: String, account: String) -> Data? { - let query = [ - kSecAttrService: service, - kSecAttrAccount: account, - kSecClass: kSecClassGenericPassword, - kSecReturnData: true - ] as CFDictionary + func loadToken() -> String? { + guard let data = KeychainManager.load(service: "com.yourapp.token", account: "userToken") else { return nil } + return String(data: data, encoding: .utf8) + } + + func deleteToken() { + KeychainManager.delete(service: "com.yourapp.token", account: "userToken") + } + + func saveMemberID(_ memberID: Int) { + let data = withUnsafeBytes(of: memberID) { Data($0) } + KeychainManager.save(data, service: "com.yourapp.memberID", account: "userID") + } + + func loadMemberID() -> Int? { + guard let data = KeychainManager.load(service: "com.yourapp.memberID", account: "userID") else { return nil } + return data.withUnsafeBytes { $0.load(as: Int.self) } + } + + func deleteMemberID() { + KeychainManager.delete(service: "com.yourapp.memberID", account: "userID") + } + + private static func save(_ data: Data, service: String, account: String) { + let query: [String: Any] = [ + kSecValueData as String: data, + kSecAttrService as String: service, + kSecAttrAccount as String: account, + kSecClass as String: kSecClassGenericPassword + ] + + SecItemDelete(query as CFDictionary) // 기존 값을 덮어쓰도록 기존 항목 삭제 + SecItemAdd(query as CFDictionary, nil) + } + + private static func load(service: String, account: String) -> Data? { + let query: [String: Any] = [ + kSecAttrService as String: service, + kSecAttrAccount as String: account, + kSecClass as String: kSecClassGenericPassword, + kSecReturnData as String: true + ] var result: AnyObject? - SecItemCopyMatching(query, &result) + SecItemCopyMatching(query as CFDictionary, &result) - return (result as? Data) + return result as? Data } - static func delete(service: String, account: String) { - let query = [ - kSecAttrService: service, - kSecAttrAccount: account, - kSecClass: kSecClassGenericPassword - ] as CFDictionary + private static func delete(service: String, account: String) { + let query: [String: Any] = [ + kSecAttrService as String: service, + kSecAttrAccount as String: account, + kSecClass as String: kSecClassGenericPassword + ] - SecItemDelete(query) + SecItemDelete(query as CFDictionary) } } diff --git a/Frontend-iOS/FebirdApp/FebirdApp/Sources/NetworkService/Member/MemberViewModel.swift b/Frontend-iOS/FebirdApp/FebirdApp/Sources/NetworkService/Member/MemberViewModel.swift index 8320aad9..725261d7 100644 --- a/Frontend-iOS/FebirdApp/FebirdApp/Sources/NetworkService/Member/MemberViewModel.swift +++ b/Frontend-iOS/FebirdApp/FebirdApp/Sources/NetworkService/Member/MemberViewModel.swift @@ -29,11 +29,11 @@ class MemberViewModel: ObservableObject { "Content-Type": "application/json", "appleID": appleID ] - + let request = AF.request(url, method: .post, headers: headers) - + let response = await request.serializingDecodable(LoginResponse.self).response - + switch response.result { case .success(let loginResponse): print("JWT 토큰: \(loginResponse.token)")