From 6dc524524077fadf52d2f0b7182314cf3ac47f72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cxixn2=E2=80=9D?= Date: Tue, 31 Dec 2024 01:54:17 +0900 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20::=20[#44]=20Tuist=20=EB=B2=84?= =?UTF-8?q?=EC=A0=84=20=EB=B3=80=EA=B2=BD=20/=20=EC=A0=84=EC=B2=B4?= =?UTF-8?q?=EC=A0=81=EC=9D=B8=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?/=20=EA=B0=81=EC=A2=85=20=EC=98=A4=EB=A5=98=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .mise.toml | 2 +- .../LoginFeature/Sources/LoginView.swift | 15 ++-- .../LoginFeature/Sources/LoginViewModel.swift | 78 +++++++++++-------- .../Sources/LocationPostViewModel.swift | 2 +- .../MainFeature/Sources/MainView.swift | 1 + .../MainFeature/Sources/MainViewModel.swift | 2 +- .../MyPageFeature/Sources/MyPageView.swift | 23 +++++- .../Sources/PostCreateView.swift | 2 +- .../Sources/PostViewModel.swift | 2 +- .../Sources/UserInfoView.swift | 48 ++++++++---- .../Sources/UserInfoViewModel.swift | 65 ++++++++++------ .../Domain/Sources/API/Emoji/EmojiAPI.swift | 2 +- .../Domain/Sources/API/Main/MainAPI.swift | 2 +- .../Domain/Sources/API/Post/PostAPI.swift | 2 +- .../Domain/Sources/API/User/UserAPI.swift | 4 +- Projects/Domain/Sources/Extension/Token.swift | 65 +++++++++------- .../Response/Post/MyInfoResponse.swift | 2 +- .../Response/Post/UserListResponse.swift | 2 +- 18 files changed, 202 insertions(+), 117 deletions(-) diff --git a/.mise.toml b/.mise.toml index 10c31de..6770e5b 100644 --- a/.mise.toml +++ b/.mise.toml @@ -1,3 +1,3 @@ [tools] -tuist = "4.32.0" +tuist = "4.9.0" diff --git a/Projects/App/Sources/Feature/LoginFeature/Sources/LoginView.swift b/Projects/App/Sources/Feature/LoginFeature/Sources/LoginView.swift index 01f34d4..9727a44 100644 --- a/Projects/App/Sources/Feature/LoginFeature/Sources/LoginView.swift +++ b/Projects/App/Sources/Feature/LoginFeature/Sources/LoginView.swift @@ -5,37 +5,40 @@ struct LoginView: View { @StateObject var userInfoViewModel: UserInfoViewModel var body: some View { + NavigationStack { ZStack { GPleAsset.Color.back.swiftUIColor .ignoresSafeArea() VStack(spacing: 0) { Spacer() - + GPleAsset.Assets.gpleLogo.swiftUIImage .padding(.bottom, 20) - + Text("GSM 사진 공유 서비스") .foregroundStyle(GPleAsset.Color.gray400.swiftUIColor) .font(GPleFontFamily.Pretendard.semiBold.swiftUIFont(size: 20)) .padding(.bottom, 90) - + Spacer() - + GPleAsset.Assets.map.swiftUIImage .resizable() .ignoresSafeArea() .frame(width: 410, height: 500) } - + VStack { Spacer() - + LoginButton(loginViewModel: viewModel) .padding(.bottom, 40) } } + } .fullScreenCover(isPresented: $viewModel.isSignedIn) { UserInfoView(viewModel: UserInfoViewModel()) } + .navigationBarBackButtonHidden(true) } } diff --git a/Projects/App/Sources/Feature/LoginFeature/Sources/LoginViewModel.swift b/Projects/App/Sources/Feature/LoginFeature/Sources/LoginViewModel.swift index 431b6b7..1175560 100644 --- a/Projects/App/Sources/Feature/LoginFeature/Sources/LoginViewModel.swift +++ b/Projects/App/Sources/Feature/LoginFeature/Sources/LoginViewModel.swift @@ -6,7 +6,7 @@ import Domain final class LoginViewModel: ObservableObject { @Published var isSignedIn: Bool = false @Published var errorMessage: String? - + private let provider = MoyaProvider() private func getGoogleClientID() -> String? { @@ -25,7 +25,6 @@ final class LoginViewModel: ObservableObject { print("Google Client ID is missing.") return } - let configuration = GIDConfiguration(clientID: clientID) GIDSignIn.sharedInstance.configuration = configuration @@ -50,70 +49,87 @@ final class LoginViewModel: ObservableObject { print("CILENT_ID: \(clientID)") print("ID Token: \(idToken)") - self?.sendIdTokenToServer(idToken) + self?.sendIdTokenToServer(idToken) { success in + if success { + print("토큰 저장 성공!") + } else { + print("토큰 저장 실패.") + } + } + } } - @MainActor - private func sendIdTokenToServer(_ idToken: String) { + public func sendIdTokenToServer(_ idToken: String, completion: @escaping (Bool) -> Void) { provider.request(.login(idToken: idToken)) { [weak self] result in switch result { case let .success(res): do { - let loginResponse = try JSONDecoder().decode(Token.self, from: res.data) - + let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) - if let accessTokenExpirationDate = dateFormatter.date(from: loginResponse.accessExpiredAt), - let refreshTokenExpirationDate = dateFormatter.date(from: loginResponse.refreshExpiredAt) { + let accessTokenExpirationDate = dateFormatter.date(from: loginResponse.accessExpiredAt) + let refreshTokenExpirationDate = dateFormatter.date(from: loginResponse.refreshExpiredAt) - let accessTokenExpirationInterval = accessTokenExpirationDate.timeIntervalSinceNow - let refreshTokenExpirationInterval = refreshTokenExpirationDate.timeIntervalSinceNow + if accessTokenExpirationDate == nil { + print("Access Token 만료 시간 변환 실패: 형식이 맞지 않음, 기본 값으로 처리됨.") + } + if refreshTokenExpirationDate == nil { + print("Refresh Token 만료 시간 변환 실패: 형식이 맞지 않음, 기본 값으로 처리됨.") + } - KeyChain.shared.saveTokenWithExpiration( - key: Const.KeyChainKey.accessToken, - token: loginResponse.accessToken, - expiresIn: accessTokenExpirationInterval - ) + let accessTokenExpirationInterval = accessTokenExpirationDate?.timeIntervalSinceNow ?? 0 + let refreshTokenExpirationInterval = refreshTokenExpirationDate?.timeIntervalSinceNow ?? 0 - KeyChain.shared.saveTokenWithExpiration( - key: Const.KeyChainKey.refreshToken, - token: loginResponse.refreshToken, - expiresIn: refreshTokenExpirationInterval - ) + UserDefaults.standard.set(loginResponse.accessToken, forKey: "accessToken") + UserDefaults.standard.set(loginResponse.refreshToken, forKey: "refreshToken") - print("Access Token 만료 날짜: \(accessTokenExpirationDate)") - print("Refresh Token 만료 날짜: \(refreshTokenExpirationDate)") - print("토큰 저장 완료") + if let savedAccessToken = UserDefaults.standard.string(forKey: "accessToken") { + print("Access Token 저장 완료: \(savedAccessToken)") } else { - print("만료 시간 변환 오류: 유효한 날짜 형식이 아님.") + print("Access Token 저장 실패") } - print("------------------------------------------------------------") - + + if let savedRefreshToken = UserDefaults.standard.string(forKey: "refreshToken") { + print("Refresh Token 저장 완료: \(savedRefreshToken)") + } else { + print("Refresh Token 저장 실패") + } + + print("Access Token 만료 날짜: \(accessTokenExpirationDate ?? Date())") + print("Refresh Token 만료 날짜: \(refreshTokenExpirationDate ?? Date())") + print("토큰 저장 완료") + DispatchQueue.main.async { - print("Access Token: \(loginResponse.accessToken)") - print("Refresh Token: \(loginResponse.refreshToken)") - print("Access Token Expiry: \(loginResponse.accessExpiredAt)") - print("Refresh Token Expiry: \(loginResponse.refreshExpiredAt)") self?.isSignedIn = true } + + completion(true) + } catch { DispatchQueue.main.async { self?.errorMessage = "Failed to parse response: \(error.localizedDescription)" } + + completion(false) } case .failure(let error): DispatchQueue.main.async { self?.errorMessage = "Request failed: \(error.localizedDescription)" } + + completion(false) } } } + + + func signOut() { GIDSignIn.sharedInstance.signOut() DispatchQueue.main.async { diff --git a/Projects/App/Sources/Feature/MainFeature/Sources/LocationPostViewModel.swift b/Projects/App/Sources/Feature/MainFeature/Sources/LocationPostViewModel.swift index 85e8bcc..9cb4e51 100644 --- a/Projects/App/Sources/Feature/MainFeature/Sources/LocationPostViewModel.swift +++ b/Projects/App/Sources/Feature/MainFeature/Sources/LocationPostViewModel.swift @@ -4,7 +4,7 @@ import Foundation final class LocationPostViewModel: ObservableObject { private let authProvider = MoyaProvider() - private var accessToken: String = "Bearer eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiIyIiwiaWF0IjoxNzM0NjYyNTg4LCJleHAiOjE3NDQ2NjI1ODh9.FG4FVQ4oikC4HNy5h7gq0QyCIjVZtceIOKwAMnkULAt4y0lX5gGIF1s2Mdj9qr1H" + private var accessToken: String = UserDefaults.standard.string(forKey: "accessToken") ?? "" @Published public var gymPostList: [Post] = [] @Published public var homePostList: [Post] = [] diff --git a/Projects/App/Sources/Feature/MainFeature/Sources/MainView.swift b/Projects/App/Sources/Feature/MainFeature/Sources/MainView.swift index 95514dd..8539d00 100644 --- a/Projects/App/Sources/Feature/MainFeature/Sources/MainView.swift +++ b/Projects/App/Sources/Feature/MainFeature/Sources/MainView.swift @@ -109,6 +109,7 @@ struct MainView: View { } } } + .navigationBarBackButtonHidden(true) } @ViewBuilder diff --git a/Projects/App/Sources/Feature/MainFeature/Sources/MainViewModel.swift b/Projects/App/Sources/Feature/MainFeature/Sources/MainViewModel.swift index 80159d6..d498d82 100644 --- a/Projects/App/Sources/Feature/MainFeature/Sources/MainViewModel.swift +++ b/Projects/App/Sources/Feature/MainFeature/Sources/MainViewModel.swift @@ -4,7 +4,7 @@ import Foundation public final class MainViewModel: ObservableObject { private let authProvider = MoyaProvider() - private var accessToken: String = "Bearer eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiIyIiwiaWF0IjoxNzM0NjYyNTg4LCJleHAiOjE3NDQ2NjI1ODh9.FG4FVQ4oikC4HNy5h7gq0QyCIjVZtceIOKwAMnkULAt4y0lX5gGIF1s2Mdj9qr1H" + private var accessToken: String = UserDefaults.standard.string(forKey: "accessToken") ?? "" @Published public var allPostList: [Post] = [] @Published public var gymPostList: [Post] = [] diff --git a/Projects/App/Sources/Feature/MyPageFeature/Sources/MyPageView.swift b/Projects/App/Sources/Feature/MyPageFeature/Sources/MyPageView.swift index 49545fd..b28be1b 100644 --- a/Projects/App/Sources/Feature/MyPageFeature/Sources/MyPageView.swift +++ b/Projects/App/Sources/Feature/MyPageFeature/Sources/MyPageView.swift @@ -5,6 +5,8 @@ struct MyPageView: View { @State private var topNavigationState = false @StateObject var postViewModel: PostViewModel @Environment(\.dismiss) private var dismiss + @State private var navigateToUserInfo = false + @State private var navigateToLogin = false var body: some View { NavigationStack { @@ -49,18 +51,31 @@ struct MyPageView: View { Spacer() Menu { - Button("프로필 변경", action: { - }) + Button("프로필 변경", action: { navigateToUserInfo = true }) .font(GPleFontFamily.Pretendard.regular.swiftUIFont(size: 16)) - Button("로그아웃", action: { /* 액션 */ }) + Button("로그아웃", action: { navigateToLogin = true }) .font(GPleFontFamily.Pretendard.regular.swiftUIFont(size: 16)) .foregroundStyle(GPleAsset.Color.system.swiftUIColor) - } label: { GPleAsset.Assets.point3.swiftUIImage .padding(.trailing, 20) .padding(.top, 8) } + + NavigationLink( + destination: UserInfoView(viewModel: UserInfoViewModel()), + isActive: $navigateToUserInfo + ) { + EmptyView() + } + + // 로그아웃 화면 네비게이션 + NavigationLink( + destination: LoginView(viewModel: LoginViewModel(), userInfoViewModel: UserInfoViewModel()), + isActive: $navigateToLogin + ) { + EmptyView() + } } VStack(alignment: topNavigationState ? .trailing : .leading, spacing: 0) { diff --git a/Projects/App/Sources/Feature/PostCreateFeature/Sources/PostCreateView.swift b/Projects/App/Sources/Feature/PostCreateFeature/Sources/PostCreateView.swift index 6fe571a..302a6c7 100644 --- a/Projects/App/Sources/Feature/PostCreateFeature/Sources/PostCreateView.swift +++ b/Projects/App/Sources/Feature/PostCreateFeature/Sources/PostCreateView.swift @@ -442,7 +442,7 @@ struct PostCreateView: View { ForEach(filteredUsers.indices, id: \.self) { index in let student = filteredUsers[index] searchUserList( - userProfileImage: student.profileImage, + userProfileImage: student.profileImage ?? "", userName: student.name, userYear: student.grade, userId: student.id diff --git a/Projects/App/Sources/Feature/PostCreateFeature/Sources/PostViewModel.swift b/Projects/App/Sources/Feature/PostCreateFeature/Sources/PostViewModel.swift index ddefcd3..2e50105 100644 --- a/Projects/App/Sources/Feature/PostCreateFeature/Sources/PostViewModel.swift +++ b/Projects/App/Sources/Feature/PostCreateFeature/Sources/PostViewModel.swift @@ -10,7 +10,7 @@ public final class PostViewModel: ObservableObject { ) private let userProvider = MoyaProvider() private var title: String = "" - private var accessToken: String = "Bearer eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiIyIiwiaWF0IjoxNzM0NjYyNTg4LCJleHAiOjE3NDQ2NjI1ODh9.FG4FVQ4oikC4HNy5h7gq0QyCIjVZtceIOKwAMnkULAt4y0lX5gGIF1s2Mdj9qr1H" + private var accessToken: String = UserDefaults.standard.string(forKey: "accessToken") ?? "" private var userList: [Int] = [] private var imageUrl: [String] = [] private var imageUrlString: [String] = [] diff --git a/Projects/App/Sources/Feature/UserInfoFeature/Sources/UserInfoView.swift b/Projects/App/Sources/Feature/UserInfoFeature/Sources/UserInfoView.swift index 988655f..cba338d 100644 --- a/Projects/App/Sources/Feature/UserInfoFeature/Sources/UserInfoView.swift +++ b/Projects/App/Sources/Feature/UserInfoFeature/Sources/UserInfoView.swift @@ -2,13 +2,15 @@ import SwiftUI struct UserInfoView: View { @Environment(\.dismiss) private var dismiss + @State private var isSuccess: Bool = false @StateObject var viewModel: UserInfoViewModel var body: some View { + NavigationStack { ZStack(alignment: .leading) { GPleAsset.Color.back.swiftUIColor .ignoresSafeArea() - + VStack(alignment: .leading, spacing: 0) { ZStack { HStack { @@ -19,7 +21,7 @@ struct UserInfoView: View { } Spacer() } - + Text("정보 입력") .foregroundStyle(.white) .font(GPleFontFamily.Pretendard.semiBold.swiftUIFont(size: 18)) @@ -27,55 +29,55 @@ struct UserInfoView: View { .padding(.bottom, 16) .padding(.top, 8) .padding(.horizontal, 20) - + VStack(spacing: 0) { HStack { Text("정보를 입력해 주세요") .font(GPleFontFamily.Pretendard.semiBold.swiftUIFont(size: 24)) .foregroundStyle(GPleAsset.Color.white.swiftUIColor) - + Spacer() } .padding(.top, 20) .padding(.bottom, 8) - + HStack { Text("원활한 서비스를 위해 정보를 입력해 주세요") .font(GPleFontFamily.Pretendard.regular.swiftUIFont(size: 16)) .foregroundStyle(GPleAsset.Color.white.swiftUIColor) - + Spacer() } .padding(.bottom, 20) - + HStack { Text("이름") .font(GPleFontFamily.Pretendard.regular.swiftUIFont(size: 14)) .foregroundStyle(GPleAsset.Color.white.swiftUIColor) - + Spacer() } .padding(.bottom, 4) } .padding(.horizontal, 20) - + GPleTextField("이름을 입력해 주세요", text: $viewModel.name) - + HStack { Text("학번") .font(GPleFontFamily.Pretendard.regular.swiftUIFont(size: 14)) .foregroundStyle(GPleAsset.Color.white.swiftUIColor) - + Spacer() } .padding(.horizontal, 20) .padding(.top, 20) .padding(.bottom, 4) - + GPleTextField("학번을 입력해 주세요", text: $viewModel.number) - + Spacer() - + GPleButton(text: "완료", horizontalPadding: 20, verticalPadding: 16, @@ -83,10 +85,26 @@ struct UserInfoView: View { buttonState: viewModel.name.isEmpty == false && viewModel.number.isEmpty == false, buttonOkColor: GPleAsset.Color.main.swiftUIColor ) { - viewModel.submitUserInfo() + viewModel.submitUserInfo { success in + if success { + isSuccess = true + print("정보 전송 성공!!") + } else { + print("정보 전송 실패!!") + } + } } .padding(.bottom, 12) + + NavigationLink( + destination: MainView(viewModel: MainViewModel(), postViewModel: PostViewModel()), + isActive: $isSuccess + ) { + EmptyView() + } } } } + .navigationBarBackButtonHidden(true) + } } diff --git a/Projects/App/Sources/Feature/UserInfoFeature/Sources/UserInfoViewModel.swift b/Projects/App/Sources/Feature/UserInfoFeature/Sources/UserInfoViewModel.swift index 5b8cde0..7e2a1a4 100644 --- a/Projects/App/Sources/Feature/UserInfoFeature/Sources/UserInfoViewModel.swift +++ b/Projects/App/Sources/Feature/UserInfoFeature/Sources/UserInfoViewModel.swift @@ -6,37 +6,58 @@ import Domain final class UserInfoViewModel: ObservableObject { @Published var name: String = "" @Published var number: String = "" - @Published var errorMessage: String? = nil private let provider = MoyaProvider() - private func fetchAccessToken() -> String? { - return KeyChain.shared.read(key: Const.KeyChainKey.accessToken) - } - - func submitUserInfo() { + func submitUserInfo(completion: @escaping (Bool) -> Void) { errorMessage = nil print("name: \(name), number: \(number)") - - guard let authorizationToken = fetchAccessToken() else { return } - - provider.request(.userInfoInput(authorization: authorizationToken, name: name, number: number, file: nil)) { [weak self] result in - print("요청 전달") - - switch result { - case .success(let response): - if (200...299).contains(response.statusCode) { - print("사용자 정보가 성공적으로 전송되었습니다.") - } else { + + if let tokenData = UserDefaults.standard.string(forKey: "accessToken") { + print("Token found: \(tokenData)") + + provider.request(.userInfoInput(authorization: tokenData, name: name, number: number, file: nil)) { [weak self] result in + print("요청 전달") + + switch result { + case .success(let response): + print("응답 상태 코드: \(response.statusCode)") + if let responseBody = String(data: response.data, encoding: .utf8) { + print("응답 본문: \(responseBody)") + } + + if (200...299).contains(response.statusCode) { + print("사용자 정보가 성공적으로 전송되었습니다.") + DispatchQueue.main.async { + completion(true) + } + } else { + DispatchQueue.main.async { + self?.errorMessage = "서버 오류: \(response.statusCode)" + } + + DispatchQueue.main.async { + completion(false) + } + } + case .failure(let error): + print("네트워크 오류: \(error.localizedDescription)") DispatchQueue.main.async { - self?.errorMessage = "서버 오류: \(response.statusCode)" + self?.errorMessage = "네트워크 오류: \(error.localizedDescription)" + } + + DispatchQueue.main.async { + completion(false) } } - case .failure(let error): - DispatchQueue.main.async { - self?.errorMessage = "네트워크 오류: \(error.localizedDescription)" - } + } + } else { + print("토큰이 없거나 만료되었습니다.") + self.errorMessage = "유효한 토큰이 없습니다." + + DispatchQueue.main.async { + completion(false) } } } diff --git a/Projects/Domain/Sources/API/Emoji/EmojiAPI.swift b/Projects/Domain/Sources/API/Emoji/EmojiAPI.swift index 64d4760..252e28d 100644 --- a/Projects/Domain/Sources/API/Emoji/EmojiAPI.swift +++ b/Projects/Domain/Sources/API/Emoji/EmojiAPI.swift @@ -38,7 +38,7 @@ extension EmojiAPI: TargetType { public var headers: [String : String]? { switch self { case .emojiPost(_, let authorization): - return ["Authorization": authorization] + return ["Authorization": "Bearer \(authorization)"] } } } diff --git a/Projects/Domain/Sources/API/Main/MainAPI.swift b/Projects/Domain/Sources/API/Main/MainAPI.swift index efe2307..ca468f2 100644 --- a/Projects/Domain/Sources/API/Main/MainAPI.swift +++ b/Projects/Domain/Sources/API/Main/MainAPI.swift @@ -53,7 +53,7 @@ extension MainAPI: TargetType { public var headers: [String : String]? { switch self { case .fetchAllPostList(let authorization), .fetchGymPostList(let authorization), .fetchPlaygroundPostList(let authorization), .fetchDommitoryPostList(let authorization), .fetchHomePostList(let authorization), .fetchWalkingTrailPostList(let authorization): - return ["Authorization": authorization] + return ["Authorization": "Bearer \(authorization)"] } } } diff --git a/Projects/Domain/Sources/API/Post/PostAPI.swift b/Projects/Domain/Sources/API/Post/PostAPI.swift index 5d67192..a4ae600 100644 --- a/Projects/Domain/Sources/API/Post/PostAPI.swift +++ b/Projects/Domain/Sources/API/Post/PostAPI.swift @@ -66,7 +66,7 @@ extension PostAPI: TargetType { public var headers: [String : String]? { switch self { case .createPost(_, let authorization), .uploadImage(_, let authorization), .allUserList(let authorization), .myPostList(let authorization), .myReactionPostList(let authorization), .popularityPostList(let authorization), .popularityUserList(let authorization): - return ["Authorization": authorization] + return ["Authorization": "Bearer \(authorization)"] } } } diff --git a/Projects/Domain/Sources/API/User/UserAPI.swift b/Projects/Domain/Sources/API/User/UserAPI.swift index d8aa263..2eb8f26 100644 --- a/Projects/Domain/Sources/API/User/UserAPI.swift +++ b/Projects/Domain/Sources/API/User/UserAPI.swift @@ -53,9 +53,7 @@ extension UserAPI: TargetType { public var headers: [String : String]? { switch self { case .userInfoInput(let authorization, _, _, _), .myInfo(let authorization): - return [ - "Authorization": "Bearer \(authorization)" - ] + return ["Authorization": "Bearer \(authorization)"] } } } diff --git a/Projects/Domain/Sources/Extension/Token.swift b/Projects/Domain/Sources/Extension/Token.swift index e64bdc9..a5147f3 100644 --- a/Projects/Domain/Sources/Extension/Token.swift +++ b/Projects/Domain/Sources/Extension/Token.swift @@ -2,40 +2,52 @@ import Moya import Security import Foundation -// 토큰과 만료 기간을 포함하는 구조체 -struct TokenData: Codable { - let token: String +public struct TokenData: Codable { + public let token: String + public let expirationDate: Date } public class KeyChain { public static let shared = KeyChain() - // 토큰 저장하기 (만료 기간과 함께) func create(key: String, token: String) { - // 먼저 토큰을 데이터로 변환하고 만료 기간을 설정 if let tokenData = token.data(using: .utf8) { let query: NSDictionary = [ kSecClass: kSecClassGenericPassword, kSecAttrAccount: key, kSecValueData: tokenData ] - SecItemDelete(query) // 기존 데이터 삭제 + SecItemDelete(query) let status = SecItemAdd(query, nil) assert(status == noErr, "failed to save Token") } } - // 토큰과 만료 기간을 JSON으로 저장하기 public func saveTokenWithExpiration(key: String, token: String, expiresIn: TimeInterval) { - let expirationDate = Date().addingTimeInterval(expiresIn) // 만료일 계산 - let tokenData = TokenData(token: token) + let expirationDate = Date().addingTimeInterval(expiresIn) + let tokenData = TokenData(token: token, expirationDate: expirationDate) - if let encodedData = try? JSONEncoder().encode(tokenData), - let tokenString = String(data: encodedData, encoding: .utf8) { - create(key: key, token: tokenString) - print("Token 저장 완료: \(tokenString)") + guard let encodedData = try? JSONEncoder().encode(tokenData) else { + print("Token encoding failed.") + return + } + + let query: [CFString: Any] = [ + kSecClass: kSecClassGenericPassword, + kSecAttrAccount: key, + kSecValueData: encodedData + ] + + let deleteStatus = SecItemDelete(query as CFDictionary) + if deleteStatus != errSecSuccess { + print("Failed to delete existing token, status: \(deleteStatus)") + } + + let status = SecItemAdd(query as CFDictionary, nil) + if status == errSecSuccess { + print("Token saved successfully to Keychain.") } else { - print("TokenData 인코딩 실패") + print("Failed to save token, status: \(status)") } } @@ -52,20 +64,16 @@ public class KeyChain { if status == errSecSuccess { if let retrievedData: Data = dataTypeRef as? Data { - if let tokenData = try? JSONDecoder().decode(TokenData.self, from: retrievedData) { - return tokenData.token - } else { - return String(data: retrievedData, encoding: .utf8) - } - } + let value = String(data: retrievedData, encoding: .utf8) + return value + } else { return nil } } else { - print("failed to load token, status code = \(status)") + print("failed to loading, status code = \(status)") + return nil } - return nil } - // 토큰과 만료 기간을 함께 읽기 - func loadTokenWithExpiration(key: String) -> TokenData? { + public func loadTokenWithExpiration(key: String) -> TokenData? { if let tokenString = read(key: key), let tokenData = tokenString.data(using: .utf8) { return try? JSONDecoder().decode(TokenData.self, from: tokenData) @@ -73,7 +81,13 @@ public class KeyChain { return nil } - // 토큰 업데이트하기 + public func isTokenExpired(key: String) -> Bool { + if let tokenData = loadTokenWithExpiration(key: key) { + return tokenData.expirationDate < Date() + } + return true + } + func updateItem(token: Any, key: Any) -> Bool { let prevQuery: [CFString: Any] = [kSecClass: kSecClassGenericPassword, kSecAttrAccount: key] @@ -90,7 +104,6 @@ public class KeyChain { return result } - // 토큰 삭제 func delete(key: String) { let query: NSDictionary = [ kSecClass: kSecClassGenericPassword, diff --git a/Projects/Domain/Sources/Response/Post/MyInfoResponse.swift b/Projects/Domain/Sources/Response/Post/MyInfoResponse.swift index c7a2c27..582aabd 100644 --- a/Projects/Domain/Sources/Response/Post/MyInfoResponse.swift +++ b/Projects/Domain/Sources/Response/Post/MyInfoResponse.swift @@ -4,5 +4,5 @@ public struct MyInfoResponse: Identifiable, Codable { public let id: Int public let grade: Int public let name: String - public let profileImage: String + public let profileImage: String? } diff --git a/Projects/Domain/Sources/Response/Post/UserListResponse.swift b/Projects/Domain/Sources/Response/Post/UserListResponse.swift index 9737505..b7f77f8 100644 --- a/Projects/Domain/Sources/Response/Post/UserListResponse.swift +++ b/Projects/Domain/Sources/Response/Post/UserListResponse.swift @@ -4,5 +4,5 @@ public struct UserListResponse: Identifiable, Codable { public let id: Int public let name: String public let grade: Int - public let profileImage: String + public let profileImage: String? }