From dffe9bad19d75a6cae62ed81ade944c928ceb6e0 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Sat, 27 Jun 2026 00:52:37 +0900 Subject: [PATCH 1/5] =?UTF-8?q?refactor:=20Firebase=20Functions=20REST=20A?= =?UTF-8?q?PI=20=ED=98=B8=EC=B6=9C=20=EA=B5=AC=EC=A1=B0=20=EC=A4=80?= =?UTF-8?q?=EB=B9=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DevLogApp/Sources/Resource/Info.plist | 2 + .../Common/FirebaseConfiguration.swift | 30 +++- .../Extension/FirebaseFunctions+.swift | 14 -- .../Sources/Service/FunctionAPIClient.swift | 163 ++++++++++++++++++ .../Sources/Service/FunctionAPIEndpoint.swift | 50 ++++++ .../Service/PushNotificationServiceImpl.swift | 17 +- .../AppleAuthenticationServiceImpl.swift | 56 +++--- .../GithubAuthenticationServiceImpl.swift | 39 ++--- .../Sources/Service/TodoServiceImpl.swift | 17 +- .../Sources/Service/WebPageServiceImpl.swift | 17 +- 10 files changed, 299 insertions(+), 106 deletions(-) delete mode 100644 Application/DevLogInfra/Sources/Extension/FirebaseFunctions+.swift create mode 100644 Application/DevLogInfra/Sources/Service/FunctionAPIClient.swift create mode 100644 Application/DevLogInfra/Sources/Service/FunctionAPIEndpoint.swift diff --git a/Application/DevLogApp/Sources/Resource/Info.plist b/Application/DevLogApp/Sources/Resource/Info.plist index 252abcda..cfc4fe6a 100644 --- a/Application/DevLogApp/Sources/Resource/Info.plist +++ b/Application/DevLogApp/Sources/Resource/Info.plist @@ -42,6 +42,8 @@ FIRESTORE_DATABASE_ID $(FIRESTORE_DATABASE_ID) + FUNCTION_API_BASE_URL + $(FUNCTION_API_BASE_URL) GIDClientID $(CLIENT_ID) GITHUB_CLIENT_ID diff --git a/Application/DevLogInfra/Sources/Common/FirebaseConfiguration.swift b/Application/DevLogInfra/Sources/Common/FirebaseConfiguration.swift index 72345e7c..a4d476ca 100644 --- a/Application/DevLogInfra/Sources/Common/FirebaseConfiguration.swift +++ b/Application/DevLogInfra/Sources/Common/FirebaseConfiguration.swift @@ -6,12 +6,12 @@ // import FirebaseFirestore -import FirebaseFunctions import Foundation enum FirebaseConfiguration { private enum InfoKey { static let databaseID = "FIRESTORE_DATABASE_ID" + static let functionAPIBaseURL = "FUNCTION_API_BASE_URL" } static let defaultDatabaseID = "staging" @@ -39,7 +39,31 @@ enum FirebaseConfiguration { Firestore.firestore(database: databaseID) } - static var functions: Functions { - Functions.functions(region: "asia-northeast3") + static func functionAPIBaseURL() throws -> URL { + if let value = resolvedValue(for: InfoKey.functionAPIBaseURL), + let url = URL(string: value) { + return url + } + + throw URLError(.badURL) + } + + private static func resolvedValue(for key: String) -> String? { + let environmentValue = ProcessInfo.processInfo.environment[key]? + .trimmingCharacters(in: .whitespacesAndNewlines) + if let environmentValue, !environmentValue.isEmpty { + return environmentValue + } + + guard let rawValue = Bundle.main.object(forInfoDictionaryKey: key) as? String else { + return nil + } + + let value = rawValue.trimmingCharacters(in: .whitespacesAndNewlines) + guard !value.isEmpty, !value.hasPrefix("$(") else { + return nil + } + + return value } } diff --git a/Application/DevLogInfra/Sources/Extension/FirebaseFunctions+.swift b/Application/DevLogInfra/Sources/Extension/FirebaseFunctions+.swift deleted file mode 100644 index d9ce43bc..00000000 --- a/Application/DevLogInfra/Sources/Extension/FirebaseFunctions+.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// FirebaseFunctions+.swift -// DevLogInfra -// -// Created by opfic on 3/16/26. -// - -import FirebaseFunctions - -extension Functions { - func httpsCallable(_ name: some RawRepresentable) -> HTTPSCallable { - httpsCallable(name.rawValue) - } -} diff --git a/Application/DevLogInfra/Sources/Service/FunctionAPIClient.swift b/Application/DevLogInfra/Sources/Service/FunctionAPIClient.swift new file mode 100644 index 00000000..148748eb --- /dev/null +++ b/Application/DevLogInfra/Sources/Service/FunctionAPIClient.swift @@ -0,0 +1,163 @@ +// +// FunctionAPIClient.swift +// DevLogInfra +// +// Created by opfic on 6/26/26. +// + +import FirebaseAuth +import Foundation +import DevLogData +import Nexa + +struct FunctionAPIClient { + private let authTokenProvider = FirebaseAuthTokenProvider() + + func send( + _ endpoint: FunctionAPIEndpoint, + payload: some Encodable, + requiresAuthentication: Bool = true + ) async throws { + var request = try client() + .request(endpoint) + .json(payload) + + if requiresAuthentication { + request = request.authorized() + } + + _ = try await request.raw() + } + + func send( + _ endpoint: FunctionAPIEndpoint, + requiresAuthentication: Bool = true + ) async throws { + var request = try client() + .request(endpoint) + + if requiresAuthentication { + request = request.authorized() + } + + _ = try await request.raw() + } + + func send( + _ endpoint: FunctionAPIEndpoint, + payload: some Encodable, + requiresAuthentication: Bool = true + ) async throws -> Response { + var request = try client() + .request(endpoint) + .json(payload) + + if requiresAuthentication { + request = request.authorized() + } + + return try await request.send() + } + + func send( + _ endpoint: FunctionAPIEndpoint, + requiresAuthentication: Bool = true + ) async throws -> Response { + try await send( + endpoint, + payload: EmptyPayload(), + requiresAuthentication: requiresAuthentication + ) + } + + private func client() throws -> NXAPIClient { + try NXAPIClient( + configuration: NXClientConfiguration( + baseURL: FirebaseConfiguration.functionAPIBaseURL(), + headers: ["Accept": "application/json"], + serverErrorDecoder: FunctionAPIServerErrorDecoder(), + authTokenProvider: authTokenProvider + ) + ) + } +} + +struct FunctionAPIEndpoint: NXEndpoint { + let method: NXHTTPMethod + let path: String +} + +struct FunctionAPIResponse: Decodable { + let accessToken: String? + let customToken: String? + let refreshToken: String? + let token: String? +} + +struct EmptyAPIResponse: Decodable {} + +private struct EmptyPayload: Encodable {} + +private struct FunctionAPIServerErrorDecoder: NXServerErrorDecoder { + func decodeServerError( + data: Data, + response: HTTPURLResponse, + decoder: JSONDecoder + ) -> (any Error)? { + guard let json = try? JSONSerialization.jsonObject(with: data), + let payload = json as? [String: Any], + let reason = reason(from: payload) else { + return nil + } + + switch reason { + case EmailFetchError.emailNotFound.code: + return EmailFetchError.emailNotFound + case EmailFetchError.emailMismatch.code: + return EmailFetchError.emailMismatch + default: + return nil + } + } + + private func reason(from payload: [String: Any]) -> String? { + if let reason = payload["reason"] as? String { + return reason + } + + if let details = payload["details"] as? [String: Any], + let reason = details["reason"] as? String { + return reason + } + + if let error = payload["error"] as? [String: Any], + let reason = error["reason"] as? String { + return reason + } + + return nil + } +} + +private actor FirebaseAuthTokenProvider: NXAuthTokenProvider { + func currentAccessToken() async throws -> String? { + try await Auth.auth().currentUser?.getIDToken() + } + + func refreshAccessToken() async throws -> String? { + try await Auth.auth().currentUser?.getIDToken(forcingRefresh: true) + } +} + +extension Error { + var apiEmailFetchError: EmailFetchError? { + guard let error = self as? NXError, + case let .server( + statusCode: _, + data: _, + underlying: underlying + ) = error else { return nil } + + return underlying as? EmailFetchError + } +} diff --git a/Application/DevLogInfra/Sources/Service/FunctionAPIEndpoint.swift b/Application/DevLogInfra/Sources/Service/FunctionAPIEndpoint.swift new file mode 100644 index 00000000..37a82dbb --- /dev/null +++ b/Application/DevLogInfra/Sources/Service/FunctionAPIEndpoint.swift @@ -0,0 +1,50 @@ +// +// FunctionAPIEndpoint.swift +// DevLogInfra +// +// Created by opfic on 6/26/26. +// + +import Foundation + +extension FunctionAPIEndpoint where Response == EmptyAPIResponse { + static func requestTodoDeletion(_ id: String) -> Self { + Self(method: .post, path: "/todos/\(functionAPIPathSegment(id))/deletion-request") + } + + static func undoTodoDeletion(_ id: String) -> Self { + Self(method: .delete, path: "/todos/\(functionAPIPathSegment(id))/deletion-request") + } + + static func requestWebPageDeletion(_ id: String) -> Self { + Self(method: .post, path: "/web-pages/\(functionAPIPathSegment(id))/deletion-request") + } + + static func undoWebPageDeletion(_ id: String) -> Self { + Self(method: .delete, path: "/web-pages/\(functionAPIPathSegment(id))/deletion-request") + } + + static func requestPushNotificationDeletion(_ id: String) -> Self { + Self(method: .post, path: "/push-notifications/\(functionAPIPathSegment(id))/deletion-request") + } + + static func undoPushNotificationDeletion(_ id: String) -> Self { + Self(method: .delete, path: "/push-notifications/\(functionAPIPathSegment(id))/deletion-request") + } + + static let revokeAppleAccessToken = Self(method: .delete, path: "/auth/apple/access-token") + static let revokeGithubAccessToken = Self(method: .delete, path: "/auth/github/access-token") +} + +extension FunctionAPIEndpoint where Response == FunctionAPIResponse { + static let requestAppleCustomToken = Self(method: .post, path: "/auth/apple/custom-token") + static let refreshAppleAccessToken = Self(method: .post, path: "/auth/apple/access-token") + static let requestAppleRefreshToken = Self(method: .post, path: "/auth/apple/refresh-token") + static let requestGithubTokens = Self(method: .post, path: "/auth/github/tokens") +} + +private func functionAPIPathSegment(_ value: String) -> String { + var allowed = CharacterSet.alphanumerics + allowed.insert(charactersIn: "-._~") + return value.addingPercentEncoding(withAllowedCharacters: allowed) ?? value +} diff --git a/Application/DevLogInfra/Sources/Service/PushNotificationServiceImpl.swift b/Application/DevLogInfra/Sources/Service/PushNotificationServiceImpl.swift index c856c688..7ac10ca7 100644 --- a/Application/DevLogInfra/Sources/Service/PushNotificationServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/PushNotificationServiceImpl.swift @@ -8,7 +8,6 @@ import FirebaseAuth import Combine import FirebaseFirestore -import FirebaseFunctions import DevLogCore import DevLogData @@ -29,13 +28,7 @@ final class PushNotificationServiceImpl: PushNotificationService { } } - private enum FunctionName: String { - case requestPushNotificationDeletion - case undoPushNotificationDeletion - } - private let store = FirebaseConfiguration.firestore - private let functions = FirebaseConfiguration.functions private let logger = Logger(category: "PushNotificationServiceImpl") /// 푸시 알림 On/Off 설정 @@ -231,8 +224,9 @@ final class PushNotificationServiceImpl: PushNotificationService { do { guard Auth.auth().currentUser?.uid != nil else { throw DataLayerError.notAuthenticated } - let function = functions.httpsCallable(FunctionName.requestPushNotificationDeletion) - _ = try await function.call(["notificationId": notificationID]) + try await FunctionAPIClient().send( + .requestPushNotificationDeletion(notificationID) + ) } catch { logger.error("Failed to request notification deletion", error: error) record(error, code: .deleteNotification) @@ -244,8 +238,9 @@ final class PushNotificationServiceImpl: PushNotificationService { do { guard Auth.auth().currentUser?.uid != nil else { throw DataLayerError.notAuthenticated } - let function = functions.httpsCallable(FunctionName.undoPushNotificationDeletion) - _ = try await function.call(["notificationId": notificationID]) + try await FunctionAPIClient().send( + .undoPushNotificationDeletion(notificationID) + ) } catch { logger.error("Failed to undo notification deletion", error: error) record(error, code: .undoDeleteNotification) diff --git a/Application/DevLogInfra/Sources/Service/SocialLogin/AppleAuthenticationServiceImpl.swift b/Application/DevLogInfra/Sources/Service/SocialLogin/AppleAuthenticationServiceImpl.swift index 3722707e..feedde42 100644 --- a/Application/DevLogInfra/Sources/Service/SocialLogin/AppleAuthenticationServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/SocialLogin/AppleAuthenticationServiceImpl.swift @@ -9,7 +9,6 @@ import AuthenticationServices import CryptoKit import FirebaseAuth import FirebaseFirestore -import FirebaseFunctions import FirebaseMessaging import Foundation import DevLogCore @@ -28,17 +27,9 @@ final class AppleAuthenticationServiceImpl: AuthenticationService { } } - private enum FunctionName: String { - case requestAppleCustomToken - case refreshAppleAccessToken - case requestAppleRefreshToken - case revokeAppleAccessToken - } - private var appleSignInDelegate: AppleSignInDelegate? private var appleSignInContinuation: CheckedContinuation? private let store = FirebaseConfiguration.firestore - private let functions = FirebaseConfiguration.functions private let messaging = Messaging.messaging() private var user: User? { Auth.auth().currentUser } private let providerID = AuthProviderID.apple @@ -275,13 +266,16 @@ final class AppleAuthenticationServiceImpl: AuthenticationService { throw URLError(.badServerResponse) } - let requestTokenFunction = functions.httpsCallable(FunctionName.requestAppleCustomToken) - let result = try await requestTokenFunction.call([ - "idToken": idToken, - "authorizationCode": authorizationCode - ]) + let response = try await FunctionAPIClient().send( + .requestAppleCustomToken, + payload: [ + "idToken": idToken, + "authorizationCode": authorizationCode + ], + requiresAuthentication: false + ) - if let data = result.data as? [String: Any], let customToken = data["customToken"] as? String { + if let customToken = response.customToken { return customToken } throw URLError(.badServerResponse) @@ -289,11 +283,9 @@ final class AppleAuthenticationServiceImpl: AuthenticationService { // Apple AceessToken 재발급 메서드 private func refreshAppleAccessToken() async throws -> String { - let refreshFunction = functions.httpsCallable(FunctionName.refreshAppleAccessToken) - let result = try await refreshFunction.call() + let response = try await FunctionAPIClient().send(.refreshAppleAccessToken) - guard let data = result.data as? [String: Any], - let accessToken = data["token"] as? String else { + guard let accessToken = response.token else { throw URLError(.cannotParseResponse) } @@ -306,26 +298,26 @@ final class AppleAuthenticationServiceImpl: AuthenticationService { throw URLError(.userAuthenticationRequired) } - let requestFuction = functions.httpsCallable(FunctionName.requestAppleRefreshToken) - - let params: [String: Any] = [ - "authorizationCode": authorizationCode, - "uid": uid - ] - - let result = try await requestFuction.call(params) + let response = try await FunctionAPIClient().send( + .requestAppleRefreshToken, + payload: [ + "authorizationCode": authorizationCode, + "uid": uid + ] + ) - if let data = result.data as? [String: Any], let accessToken = data["refreshToken"] as? String { - return accessToken + if let refreshToken = response.refreshToken { + return refreshToken } throw URLError(.badServerResponse) } // Apple AccessToken 취소 메서드 func revokeAppleAccessToken(token: String) async throws { - let revokeFunction = functions.httpsCallable(FunctionName.revokeAppleAccessToken) - - _ = try await revokeFunction.call(["token": token]) + try await FunctionAPIClient().send( + .revokeAppleAccessToken, + payload: ["token": token] + ) } } diff --git a/Application/DevLogInfra/Sources/Service/SocialLogin/GithubAuthenticationServiceImpl.swift b/Application/DevLogInfra/Sources/Service/SocialLogin/GithubAuthenticationServiceImpl.swift index f02da95e..0687e7bc 100644 --- a/Application/DevLogInfra/Sources/Service/SocialLogin/GithubAuthenticationServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/SocialLogin/GithubAuthenticationServiceImpl.swift @@ -9,7 +9,6 @@ import AuthenticationServices import Foundation import FirebaseAuth import FirebaseFirestore -import FirebaseFunctions import FirebaseMessaging import Nexa import DevLogCore @@ -28,18 +27,12 @@ final class GithubAuthenticationServiceImpl: NSObject, AuthenticationService { } } - private enum FunctionName: String { - case requestGithubTokens - case revokeGithubAccessToken - } - private enum GitHubAPI { static let baseURL = URL(string: "https://api.github.com")! static let acceptHeader = "application/vnd.github.v3+json" } private let store = FirebaseConfiguration.firestore - private let functions = FirebaseConfiguration.functions private let messaging = Messaging.messaging() private var user: User? { Auth.auth().currentUser } private let providerID = AuthProviderID.gitHub @@ -251,14 +244,15 @@ final class GithubAuthenticationServiceImpl: NSObject, AuthenticationService { // Firebase Function 호출: Custom Token 발급 private func requestTokens(authorizationCode: String) async throws -> (String, String) { - let requestTokenFunction = functions.httpsCallable(FunctionName.requestGithubTokens) - do { - let result = try await requestTokenFunction.call(["code": authorizationCode]) + let response = try await FunctionAPIClient().send( + .requestGithubTokens, + payload: ["code": authorizationCode], + requiresAuthentication: false + ) - if let data = result.data as? [String: Any], - let accessToken = data["accessToken"] as? String, - let customToken = data["customToken"] as? String { + if let accessToken = response.accessToken, + let customToken = response.customToken { return (accessToken, customToken) } throw TokenError.invalidResponse @@ -268,15 +262,16 @@ final class GithubAuthenticationServiceImpl: NSObject, AuthenticationService { } private func revokeAccessToken(accessToken: String? = nil) async throws { - var param: [String: Any] = [:] + var param: [String: String] = [:] if let accessToken = accessToken { param["accessToken"] = accessToken } - let revokeFunction = functions.httpsCallable(FunctionName.revokeGithubAccessToken) - - _ = try await revokeFunction.call(param) + try await FunctionAPIClient().send( + .revokeGithubAccessToken, + payload: param + ) } // GitHub API로 사용자 프로필 정보 가져오기 @@ -315,15 +310,11 @@ final class GithubAuthenticationServiceImpl: NSObject, AuthenticationService { } private func mapRequestTokensError(_ error: Error) -> Error { - let nsError = error as NSError - guard nsError.domain == FunctionsErrorDomain, - let details = nsError.userInfo[FunctionsErrorDetailsKey] as? [String: Any], - let reason = details["reason"] as? String, - reason == EmailFetchError.emailNotFound.code else { - return error + if let emailFetchError = error.apiEmailFetchError { + return emailFetchError } - return EmailFetchError.emailNotFound + return error } } diff --git a/Application/DevLogInfra/Sources/Service/TodoServiceImpl.swift b/Application/DevLogInfra/Sources/Service/TodoServiceImpl.swift index fd84aa95..8c1a717e 100644 --- a/Application/DevLogInfra/Sources/Service/TodoServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/TodoServiceImpl.swift @@ -7,7 +7,6 @@ import FirebaseAuth import FirebaseFirestore -import FirebaseFunctions import DevLogCore import DevLogData @@ -25,13 +24,7 @@ final class TodoServiceImpl: TodoService { } } - private enum FunctionName: String { - case requestTodoDeletion - case undoTodoDeletion - } - private let store = FirebaseConfiguration.firestore - private let functions = FirebaseConfiguration.functions private let encoder = Firestore.Encoder() private let logger = Logger(category: "TodoServiceImpl") @@ -222,8 +215,9 @@ final class TodoServiceImpl: TodoService { logger.info("Requesting todo deletion") do { - let function = functions.httpsCallable(FunctionName.requestTodoDeletion) - _ = try await function.call(["todoId": todoId]) + try await FunctionAPIClient().send( + .requestTodoDeletion(todoId) + ) logger.info("Successfully requested todo deletion") } catch { @@ -239,8 +233,9 @@ final class TodoServiceImpl: TodoService { logger.info("Undoing todo deletion") do { - let function = functions.httpsCallable(FunctionName.undoTodoDeletion) - _ = try await function.call(["todoId": todoId]) + try await FunctionAPIClient().send( + .undoTodoDeletion(todoId) + ) logger.info("Successfully undone todo deletion") } catch { diff --git a/Application/DevLogInfra/Sources/Service/WebPageServiceImpl.swift b/Application/DevLogInfra/Sources/Service/WebPageServiceImpl.swift index 6053cbf9..23d07e5f 100644 --- a/Application/DevLogInfra/Sources/Service/WebPageServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/WebPageServiceImpl.swift @@ -7,7 +7,6 @@ import FirebaseAuth import FirebaseFirestore -import FirebaseFunctions import DevLogCore import DevLogData @@ -23,13 +22,7 @@ final class WebPageServiceImpl: WebPageService { } } - private enum FunctionName: String { - case requestWebPageDeletion - case undoWebPageDeletion - } - private let store = FirebaseConfiguration.firestore - private let functions = FirebaseConfiguration.functions private let encoder = Firestore.Encoder() private let logger = Logger(category: "WebPageServiceImpl") @@ -98,8 +91,9 @@ final class WebPageServiceImpl: WebPageService { } do { - let function = functions.httpsCallable(FunctionName.requestWebPageDeletion) - _ = try await function.call(["urlString": urlString]) + try await FunctionAPIClient().send( + .requestWebPageDeletion(documentID(for: urlString)) + ) logger.info("Successfully requested web page deletion") } catch { logger.error("Failed to request web page deletion", error: error) @@ -117,8 +111,9 @@ final class WebPageServiceImpl: WebPageService { } do { - let function = functions.httpsCallable(FunctionName.undoWebPageDeletion) - _ = try await function.call(["urlString": urlString]) + try await FunctionAPIClient().send( + .undoWebPageDeletion(documentID(for: urlString)) + ) logger.info("Successfully undone web page deletion") } catch { logger.error("Failed to undo web page deletion", error: error) From 7cca6c289014d227dea8895251cd6047d557eb94 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Sat, 27 Jun 2026 00:56:34 +0900 Subject: [PATCH 2/5] =?UTF-8?q?refactor:=20REST=20API=20=EC=97=90=EB=9F=AC?= =?UTF-8?q?=20=EC=9D=91=EB=8B=B5=20=EB=A7=A4=ED=95=91=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Service/FunctionAPIClient.swift | 34 ++++++------------- 1 file changed, 10 insertions(+), 24 deletions(-) diff --git a/Application/DevLogInfra/Sources/Service/FunctionAPIClient.swift b/Application/DevLogInfra/Sources/Service/FunctionAPIClient.swift index 148748eb..d66a1cd3 100644 --- a/Application/DevLogInfra/Sources/Service/FunctionAPIClient.swift +++ b/Application/DevLogInfra/Sources/Service/FunctionAPIClient.swift @@ -98,19 +98,23 @@ struct EmptyAPIResponse: Decodable {} private struct EmptyPayload: Encodable {} +private struct FunctionAPIErrorBody: Decodable { + let code: String + let message: String? +} + private struct FunctionAPIServerErrorDecoder: NXServerErrorDecoder { func decodeServerError( data: Data, response: HTTPURLResponse, decoder: JSONDecoder ) -> (any Error)? { - guard let json = try? JSONSerialization.jsonObject(with: data), - let payload = json as? [String: Any], - let reason = reason(from: payload) else { - return nil - } + guard let body = try? decoder.decode( + FunctionAPIErrorBody.self, + from: data + ) else { return nil } - switch reason { + switch body.code { case EmailFetchError.emailNotFound.code: return EmailFetchError.emailNotFound case EmailFetchError.emailMismatch.code: @@ -119,24 +123,6 @@ private struct FunctionAPIServerErrorDecoder: NXServerErrorDecoder { return nil } } - - private func reason(from payload: [String: Any]) -> String? { - if let reason = payload["reason"] as? String { - return reason - } - - if let details = payload["details"] as? [String: Any], - let reason = details["reason"] as? String { - return reason - } - - if let error = payload["error"] as? [String: Any], - let reason = error["reason"] as? String { - return reason - } - - return nil - } } private actor FirebaseAuthTokenProvider: NXAuthTokenProvider { From 6d5f9e53d2c3020a1d1148686af8c686cef702b0 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Sat, 27 Jun 2026 11:57:03 +0900 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20WebPage=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=EC=97=90=20=EC=8B=A4=EC=A0=9C=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=20ID=20=EC=A0=84=EB=8B=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Mapper/WebPageMapping.swift | 1 + .../Sources/Protocol/WebPageService.swift | 4 +- .../Repository/WebPageRepositoryImpl.swift | 8 ++-- .../DevLogDomain/Sources/Entity/WebPage.swift | 3 ++ .../Sources/Protocol/WebPageRepository.swift | 4 +- .../WebPage/Upsert/DeleteWebPageUseCase.swift | 2 +- .../Upsert/DeleteWebPageUseCaseImpl.swift | 4 +- .../Upsert/UndoDeleteWebPageUseCase.swift | 2 +- .../Upsert/UndoDeleteWebPageUseCaseImpl.swift | 4 +- .../Sources/Service/WebPageServiceImpl.swift | 38 +++++++++---------- .../Home/Home/HomeFeature+Effects.swift | 15 ++++---- .../Sources/Home/Home/HomeFeature.swift | 36 +++++++++++------- .../Sources/Structure/WebPageItem.swift | 2 +- .../Tests/Home/HomeFeatureTestSupport.swift | 2 + .../Tests/Home/HomeFeatureTests.swift | 9 +++-- .../Search/SearchFeatureTestDoubles.swift | 2 + .../Tests/Support/TestSupport.swift | 12 +++--- 17 files changed, 83 insertions(+), 65 deletions(-) diff --git a/Application/DevLogData/Sources/Mapper/WebPageMapping.swift b/Application/DevLogData/Sources/Mapper/WebPageMapping.swift index 668c3d09..0fd4f972 100644 --- a/Application/DevLogData/Sources/Mapper/WebPageMapping.swift +++ b/Application/DevLogData/Sources/Mapper/WebPageMapping.swift @@ -23,6 +23,7 @@ public extension WebPageResponse { imageURL = nil } return WebPage( + id: id, title: title, url: url, displayURL: displayURL, diff --git a/Application/DevLogData/Sources/Protocol/WebPageService.swift b/Application/DevLogData/Sources/Protocol/WebPageService.swift index 17cdab35..27141350 100644 --- a/Application/DevLogData/Sources/Protocol/WebPageService.swift +++ b/Application/DevLogData/Sources/Protocol/WebPageService.swift @@ -10,6 +10,6 @@ import Foundation public protocol WebPageService { func fetchWebPages(_ query: String) async throws -> [WebPageResponse] func upsertWebPage(_ request: WebPageRequest) async throws - func deleteWebPage(_ urlString: String) async throws - func undoDeleteWebPage(_ urlString: String) async throws + func deleteWebPage(_ id: String) async throws + func undoDeleteWebPage(_ id: String) async throws } diff --git a/Application/DevLogData/Sources/Repository/WebPageRepositoryImpl.swift b/Application/DevLogData/Sources/Repository/WebPageRepositoryImpl.swift index f15fc3f4..dd166f1b 100644 --- a/Application/DevLogData/Sources/Repository/WebPageRepositoryImpl.swift +++ b/Application/DevLogData/Sources/Repository/WebPageRepositoryImpl.swift @@ -63,18 +63,18 @@ final class WebPageRepositoryImpl: WebPageRepository { } } - func delete(_ urlString: String) async throws { + func delete(id: String, urlString: String) async throws { do { - try await webPageService.deleteWebPage(urlString) + try await webPageService.deleteWebPage(id) await metadataService.removeCachedImage(for: urlString) } catch { throw error.toDomain() } } - func undoDelete(_ urlString: String) async throws { + func undoDelete(_ id: String) async throws { do { - try await webPageService.undoDeleteWebPage(urlString) + try await webPageService.undoDeleteWebPage(id) } catch { throw error.toDomain() } diff --git a/Application/DevLogDomain/Sources/Entity/WebPage.swift b/Application/DevLogDomain/Sources/Entity/WebPage.swift index 1c6993d5..8cc720c8 100644 --- a/Application/DevLogDomain/Sources/Entity/WebPage.swift +++ b/Application/DevLogDomain/Sources/Entity/WebPage.swift @@ -8,17 +8,20 @@ import Foundation public struct WebPage: Hashable { + public let id: String public let title: String? public let url: URL public let displayURL: URL public let imageURL: URL? public init( + id: String, title: String?, url: URL, displayURL: URL, imageURL: URL? ) { + self.id = id self.title = title self.url = url self.displayURL = displayURL diff --git a/Application/DevLogDomain/Sources/Protocol/WebPageRepository.swift b/Application/DevLogDomain/Sources/Protocol/WebPageRepository.swift index e27300f1..e3126dbb 100644 --- a/Application/DevLogDomain/Sources/Protocol/WebPageRepository.swift +++ b/Application/DevLogDomain/Sources/Protocol/WebPageRepository.swift @@ -8,6 +8,6 @@ public protocol WebPageRepository { func fetch(_ query: String) async throws -> [WebPage] func upsert(_ urlString: String) async throws - func delete(_ urlString: String) async throws - func undoDelete(_ urlString: String) async throws + func delete(id: String, urlString: String) async throws + func undoDelete(_ id: String) async throws } diff --git a/Application/DevLogDomain/Sources/UseCase/WebPage/Upsert/DeleteWebPageUseCase.swift b/Application/DevLogDomain/Sources/UseCase/WebPage/Upsert/DeleteWebPageUseCase.swift index ba05568a..af714538 100644 --- a/Application/DevLogDomain/Sources/UseCase/WebPage/Upsert/DeleteWebPageUseCase.swift +++ b/Application/DevLogDomain/Sources/UseCase/WebPage/Upsert/DeleteWebPageUseCase.swift @@ -6,5 +6,5 @@ // public protocol DeleteWebPageUseCase { - func execute(_ urlString: String) async throws + func execute(id: String, urlString: String) async throws } diff --git a/Application/DevLogDomain/Sources/UseCase/WebPage/Upsert/DeleteWebPageUseCaseImpl.swift b/Application/DevLogDomain/Sources/UseCase/WebPage/Upsert/DeleteWebPageUseCaseImpl.swift index baa112fc..1adc3e1c 100644 --- a/Application/DevLogDomain/Sources/UseCase/WebPage/Upsert/DeleteWebPageUseCaseImpl.swift +++ b/Application/DevLogDomain/Sources/UseCase/WebPage/Upsert/DeleteWebPageUseCaseImpl.swift @@ -12,7 +12,7 @@ public final class DeleteWebPageUseCaseImpl: DeleteWebPageUseCase { self.repository = repository } - public func execute(_ urlString: String) async throws { - try await repository.delete(urlString) + public func execute(id: String, urlString: String) async throws { + try await repository.delete(id: id, urlString: urlString) } } diff --git a/Application/DevLogDomain/Sources/UseCase/WebPage/Upsert/UndoDeleteWebPageUseCase.swift b/Application/DevLogDomain/Sources/UseCase/WebPage/Upsert/UndoDeleteWebPageUseCase.swift index 85d60ea1..1464d2b5 100644 --- a/Application/DevLogDomain/Sources/UseCase/WebPage/Upsert/UndoDeleteWebPageUseCase.swift +++ b/Application/DevLogDomain/Sources/UseCase/WebPage/Upsert/UndoDeleteWebPageUseCase.swift @@ -6,5 +6,5 @@ // public protocol UndoDeleteWebPageUseCase { - func execute(_ urlString: String) async throws + func execute(_ id: String) async throws } diff --git a/Application/DevLogDomain/Sources/UseCase/WebPage/Upsert/UndoDeleteWebPageUseCaseImpl.swift b/Application/DevLogDomain/Sources/UseCase/WebPage/Upsert/UndoDeleteWebPageUseCaseImpl.swift index 7ffe1a40..508f47b1 100644 --- a/Application/DevLogDomain/Sources/UseCase/WebPage/Upsert/UndoDeleteWebPageUseCaseImpl.swift +++ b/Application/DevLogDomain/Sources/UseCase/WebPage/Upsert/UndoDeleteWebPageUseCaseImpl.swift @@ -12,7 +12,7 @@ public final class UndoDeleteWebPageUseCaseImpl: UndoDeleteWebPageUseCase { self.repository = repository } - public func execute(_ urlString: String) async throws { - try await repository.undoDelete(urlString) + public func execute(_ id: String) async throws { + try await repository.undoDelete(id) } } diff --git a/Application/DevLogInfra/Sources/Service/WebPageServiceImpl.swift b/Application/DevLogInfra/Sources/Service/WebPageServiceImpl.swift index 23d07e5f..3c4369df 100644 --- a/Application/DevLogInfra/Sources/Service/WebPageServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/WebPageServiceImpl.swift @@ -70,8 +70,8 @@ final class WebPageServiceImpl: WebPageService { } do { - let documentID = documentID(for: request.url) - let docRef = store.document(FirestorePath.webPage(uid, documentId: documentID)) + let docID = documentID(for: request.url) + let docRef = store.document(FirestorePath.webPage(uid, documentId: docID)) let data = try encoder.encode(request) try await docRef.setData(data, merge: true) logger.info("Successfully upserted web page") @@ -82,8 +82,8 @@ final class WebPageServiceImpl: WebPageService { } } - func deleteWebPage(_ urlString: String) async throws { - logger.info("Requesting web page deletion: \(urlString)") + func deleteWebPage(_ id: String) async throws { + logger.info("Requesting web page deletion: \(id)") guard Auth.auth().currentUser?.uid != nil else { logger.error("User not authenticated") @@ -92,7 +92,7 @@ final class WebPageServiceImpl: WebPageService { do { try await FunctionAPIClient().send( - .requestWebPageDeletion(documentID(for: urlString)) + .requestWebPageDeletion(id) ) logger.info("Successfully requested web page deletion") } catch { @@ -102,8 +102,8 @@ final class WebPageServiceImpl: WebPageService { } } - func undoDeleteWebPage(_ urlString: String) async throws { - logger.info("Undoing web page deletion: \(urlString)") + func undoDeleteWebPage(_ id: String) async throws { + logger.info("Undoing web page deletion: \(id)") guard Auth.auth().currentUser?.uid != nil else { logger.error("User not authenticated") @@ -112,7 +112,7 @@ final class WebPageServiceImpl: WebPageService { do { try await FunctionAPIClient().send( - .undoWebPageDeletion(documentID(for: urlString)) + .undoWebPageDeletion(id) ) logger.info("Successfully undone web page deletion") } catch { @@ -121,17 +121,6 @@ final class WebPageServiceImpl: WebPageService { throw error } } - - private func documentID(for url: String) -> String { - if let encoded = url.addingPercentEncoding(withAllowedCharacters: .alphanumerics) { - return encoded - } - let base64 = Data(url.utf8).base64EncodedString() - return base64 - .replacingOccurrences(of: "/", with: "_") - .replacingOccurrences(of: "+", with: "-") - .replacingOccurrences(of: "=", with: "") - } } private extension WebPageServiceImpl { @@ -147,6 +136,17 @@ private extension WebPageServiceImpl { Self.record(error, code: code) } + func documentID(for url: String) -> String { + if let encoded = url.addingPercentEncoding(withAllowedCharacters: .alphanumerics) { + return encoded + } + let base64 = Data(url.utf8).base64EncodedString() + return base64 + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "=", with: "") + } + func makeResponse(from snapshot: QueryDocumentSnapshot) -> WebPageResponse? { let data = snapshot.data() guard diff --git a/Application/DevLogPresentation/Sources/Home/Home/HomeFeature+Effects.swift b/Application/DevLogPresentation/Sources/Home/Home/HomeFeature+Effects.swift index 32ffbbd8..dd8e6984 100644 --- a/Application/DevLogPresentation/Sources/Home/Home/HomeFeature+Effects.swift +++ b/Application/DevLogPresentation/Sources/Home/Home/HomeFeature+Effects.swift @@ -87,7 +87,10 @@ extension HomeFeature { func deleteWebPageEffect(_ page: WebPageItem) -> Effect { .run { [deleteWebPageUseCase] send in do { - try await deleteWebPageUseCase.execute(page.url.absoluteString) + try await deleteWebPageUseCase.execute( + id: page.id, + urlString: page.url.absoluteString + ) } catch { await send(.store(.handleWebPageDeleteFailure(page.id))) await send(.store(.setAlert(isPresented: true, type: .error))) @@ -95,15 +98,13 @@ extension HomeFeature { } } - func undoDeleteWebPageEffect(_ urlString: String) -> Effect { + func undoDeleteWebPageEffect(_ webPage: DeletedWebPage) -> Effect { .run { [undoDeleteWebPageUseCase, addWebPageUseCase] send in do { - try await undoDeleteWebPageUseCase.execute(urlString) - try await addWebPageUseCase.execute(urlString) + try await undoDeleteWebPageUseCase.execute(webPage.id) + try await addWebPageUseCase.execute(webPage.urlString) } catch { - if let webPageURL = URL(string: urlString) { - await send(.store(.setWebPageHidden(webPageURL, true))) - } + await send(.store(.setWebPageHidden(webPage.id, true))) await send(.store(.setAlert(isPresented: true, type: .error))) } } diff --git a/Application/DevLogPresentation/Sources/Home/Home/HomeFeature.swift b/Application/DevLogPresentation/Sources/Home/Home/HomeFeature.swift index 2edde907..2844fa3a 100644 --- a/Application/DevLogPresentation/Sources/Home/Home/HomeFeature.swift +++ b/Application/DevLogPresentation/Sources/Home/Home/HomeFeature.swift @@ -23,7 +23,7 @@ struct HomeFeature { var isNetworkConnected = true var webPageURLInput = "https://" var selectedTodoCategory: TodoCategory? - var deletedWebPageURLString: String? + var deletedWebPage: DeletedWebPage? var loading = LoadingFeature.State() var showContentPicker: Bool { sheet?.contentPickerState != nil } @@ -46,6 +46,11 @@ struct HomeFeature { } } + struct DeletedWebPage: Equatable { + let id: String + let urlString: String + } + enum Action: Equatable { case alert(PresentationAction) case sheet(PresentationAction) @@ -73,8 +78,8 @@ struct HomeFeature { case setSheet(SheetState?) case setPresentation(Presentation, Bool) case setAlert(isPresented: Bool, type: AlertType? = nil) - case setWebPageHidden(URL, Bool) - case handleWebPageDeleteFailure(URL) + case setWebPageHidden(String, Bool) + case handleWebPageDeleteFailure(String) case setTodoCategory([TodoCategoryItem]) case updateRecentTodos([RecentTodoItem]) case updateWebPages([WebPageItem]) @@ -234,8 +239,8 @@ private extension HomeFeature { return fetchWebPagesEffect() case .finishDeleteWebPageToast(let urlString): state.webPages.removeAll { $0.url.absoluteString == urlString && $0.isHidden } - if state.deletedWebPageURLString == urlString { - state.deletedWebPageURLString = nil + if state.deletedWebPage?.urlString == urlString { + state.deletedWebPage = nil } case .tapTodoCategory(let category): state.selectedTodoCategory = category @@ -259,16 +264,19 @@ private extension HomeFeature { guard let index = state.webPages.firstIndex(where: { $0.id == page.id }) else { return .none } - state.deletedWebPageURLString = page.url.absoluteString + state.deletedWebPage = DeletedWebPage( + id: page.id, + urlString: page.url.absoluteString + ) state.webPages[index].isHidden = true return deleteWebPageEffect(page) case .undoDeleteWebPage: - guard let urlString = state.deletedWebPageURLString else { return .none } - if let index = state.webPages.firstIndex(where: { $0.url.absoluteString == urlString }) { + guard let webPage = state.deletedWebPage else { return .none } + if let index = state.webPages.firstIndex(where: { $0.id == webPage.id }) { state.webPages[index].isHidden = false } - state.deletedWebPageURLString = nil - return undoDeleteWebPageEffect(urlString) + state.deletedWebPage = nil + return undoDeleteWebPageEffect(webPage) } return .none @@ -287,12 +295,12 @@ private extension HomeFeature { Self.setPresentation(&state, presentation: presentation, isPresented: isPresented) case .setAlert(let isPresented, let type): Self.setAlert(&state, isPresented: isPresented, type: type) - case .setWebPageHidden(let webPageURL, let isHidden): - if let index = state.webPages.firstIndex(where: { $0.id == webPageURL }) { + case .setWebPageHidden(let id, let isHidden): + if let index = state.webPages.firstIndex(where: { $0.id == id }) { state.webPages[index].isHidden = isHidden } - case .handleWebPageDeleteFailure(let webPageURL): - if let index = state.webPages.firstIndex(where: { $0.id == webPageURL }) { + case .handleWebPageDeleteFailure(let id): + if let index = state.webPages.firstIndex(where: { $0.id == id }) { state.webPages[index].isHidden = false } else { state.needsWebPageRefresh = true diff --git a/Application/DevLogPresentation/Sources/Structure/WebPageItem.swift b/Application/DevLogPresentation/Sources/Structure/WebPageItem.swift index 666d1aaf..510e28a1 100644 --- a/Application/DevLogPresentation/Sources/Structure/WebPageItem.swift +++ b/Application/DevLogPresentation/Sources/Structure/WebPageItem.swift @@ -16,7 +16,7 @@ public struct WebPageItem: Identifiable, Hashable { self.metadata = metadata } - public var id: URL { metadata.url } + public var id: String { metadata.id } public var title: String { metadata.title ?? String(localized: "web_page_missing_title") } public var url: URL { metadata.url } public var displayURL: String { metadata.displayURL.absoluteString } diff --git a/Application/DevLogPresentation/Tests/Home/HomeFeatureTestSupport.swift b/Application/DevLogPresentation/Tests/Home/HomeFeatureTestSupport.swift index 80e84efa..7f8c2386 100644 --- a/Application/DevLogPresentation/Tests/Home/HomeFeatureTestSupport.swift +++ b/Application/DevLogPresentation/Tests/Home/HomeFeatureTestSupport.swift @@ -185,11 +185,13 @@ func makeHomeTodo( } func makeHomeWebPage( + id: String = "web-page-id", title: String = "OpenAI", urlString: String = "https://openai.com" ) -> WebPage { let url = URL(string: urlString)! return WebPage( + id: id, title: title, url: url, displayURL: url, diff --git a/Application/DevLogPresentation/Tests/Home/HomeFeatureTests.swift b/Application/DevLogPresentation/Tests/Home/HomeFeatureTests.swift index a7da6f02..11ca6379 100644 --- a/Application/DevLogPresentation/Tests/Home/HomeFeatureTests.swift +++ b/Application/DevLogPresentation/Tests/Home/HomeFeatureTests.swift @@ -109,10 +109,11 @@ struct HomeFeatureTests { #expect(adapter.webPages.filter { !$0.isHidden }.isEmpty) await waitUntil { - context.deleteWebPageUseCaseSpy.calledUrlStrings == ["https://openai.com"] + context.deleteWebPageUseCaseSpy.calls.count == 1 } - #expect(context.deleteWebPageUseCaseSpy.calledUrlStrings == ["https://openai.com"]) + #expect(context.deleteWebPageUseCaseSpy.calls.first?.id == "web-page-id") + #expect(context.deleteWebPageUseCaseSpy.calls.first?.urlString == "https://openai.com") } @Test("웹페이지 삭제를 되돌리면 되돌리기 유스케이스가 호출되고 숨김 상태가 해제된다") @@ -133,14 +134,14 @@ struct HomeFeatureTests { await adapter.undoDeleteWebPage() await waitUntil { - context.undoDeleteWebPageUseCaseSpy.calledUrlStrings == ["https://openai.com"] + context.undoDeleteWebPageUseCaseSpy.calledIDs == ["web-page-id"] } let restoredWebPageItem = try #require(adapter.webPages.first { $0.url.absoluteString == "https://openai.com" }) - #expect(context.undoDeleteWebPageUseCaseSpy.calledUrlStrings == ["https://openai.com"]) + #expect(context.undoDeleteWebPageUseCaseSpy.calledIDs == ["web-page-id"]) #expect(!restoredWebPageItem.isHidden) } diff --git a/Application/DevLogPresentation/Tests/Search/SearchFeatureTestDoubles.swift b/Application/DevLogPresentation/Tests/Search/SearchFeatureTestDoubles.swift index 6853a8c8..cc058ed8 100644 --- a/Application/DevLogPresentation/Tests/Search/SearchFeatureTestDoubles.swift +++ b/Application/DevLogPresentation/Tests/Search/SearchFeatureTestDoubles.swift @@ -269,11 +269,13 @@ func makeSearchTodo( } func makeSearchWebPage( + id: String = "web-page-id", title: String? = "Web", urlString: String = "https://example.com" ) -> WebPage { let url = URL(string: urlString)! return WebPage( + id: id, title: title, url: url, displayURL: url, diff --git a/Application/DevLogPresentation/Tests/Support/TestSupport.swift b/Application/DevLogPresentation/Tests/Support/TestSupport.swift index 42b23d07..1ebf5afb 100644 --- a/Application/DevLogPresentation/Tests/Support/TestSupport.swift +++ b/Application/DevLogPresentation/Tests/Support/TestSupport.swift @@ -167,18 +167,18 @@ final class AddWebPageUseCaseSpy: AddWebPageUseCase { } final class DeleteWebPageUseCaseSpy: DeleteWebPageUseCase { - private(set) var calledUrlStrings: [String] = [] + private(set) var calls: [(id: String, urlString: String)] = [] - func execute(_ urlString: String) async throws { - calledUrlStrings.append(urlString) + func execute(id: String, urlString: String) async throws { + calls.append((id, urlString)) } } final class UndoDeleteWebPageUseCaseSpy: UndoDeleteWebPageUseCase { - private(set) var calledUrlStrings: [String] = [] + private(set) var calledIDs: [String] = [] - func execute(_ urlString: String) async throws { - calledUrlStrings.append(urlString) + func execute(_ id: String) async throws { + calledIDs.append(id) } } From dc50a8e1c2463f2eaae7df2b84168ab72b1308cf Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Sat, 27 Jun 2026 12:14:30 +0900 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20Apple=20refresh=20token=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=20uid=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SocialLogin/AppleAuthenticationServiceImpl.swift | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Application/DevLogInfra/Sources/Service/SocialLogin/AppleAuthenticationServiceImpl.swift b/Application/DevLogInfra/Sources/Service/SocialLogin/AppleAuthenticationServiceImpl.swift index feedde42..c40b30de 100644 --- a/Application/DevLogInfra/Sources/Service/SocialLogin/AppleAuthenticationServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/SocialLogin/AppleAuthenticationServiceImpl.swift @@ -146,7 +146,7 @@ final class AppleAuthenticationServiceImpl: AuthenticationService { let authorizationCode = response.authorizationCode let idTokenString = response.idTokenString - let refreshToken = try await requestAppleRefreshToken(uid: uid, authorizationCode: authorizationCode) + let refreshToken = try await requestAppleRefreshToken(authorizationCode: authorizationCode) guard let appleEmail = credential.email else { try await revokeAppleAccessToken(token: refreshToken) @@ -293,17 +293,14 @@ final class AppleAuthenticationServiceImpl: AuthenticationService { } // Apple RefreshToken 발급 메서드 - func requestAppleRefreshToken(uid: String, authorizationCode: Data) async throws -> String { + func requestAppleRefreshToken(authorizationCode: Data) async throws -> String { guard let authorizationCode = String(data: authorizationCode, encoding: .utf8) else { throw URLError(.userAuthenticationRequired) } let response = try await FunctionAPIClient().send( .requestAppleRefreshToken, - payload: [ - "authorizationCode": authorizationCode, - "uid": uid - ] + payload: ["authorizationCode": authorizationCode] ) if let refreshToken = response.refreshToken { From 8e5ae289c7d656f4a09c2b3996e489609593b652 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Sat, 27 Jun 2026 13:02:24 +0900 Subject: [PATCH 5/5] =?UTF-8?q?refactor:=20Function=20API=20=ED=81=B4?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EC=96=B8=ED=8A=B8=20=EA=B3=B5=EC=9C=A0=20?= =?UTF-8?q?=EC=9D=B8=EC=8A=A4=ED=84=B4=EC=8A=A4=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Service/FunctionAPIClient.swift | 29 ++++++++++++------- .../Service/PushNotificationServiceImpl.swift | 4 +-- .../AppleAuthenticationServiceImpl.swift | 8 ++--- .../GithubAuthenticationServiceImpl.swift | 4 +-- .../Sources/Service/TodoServiceImpl.swift | 4 +-- .../Sources/Service/WebPageServiceImpl.swift | 4 +-- 6 files changed, 31 insertions(+), 22 deletions(-) diff --git a/Application/DevLogInfra/Sources/Service/FunctionAPIClient.swift b/Application/DevLogInfra/Sources/Service/FunctionAPIClient.swift index d66a1cd3..7cc7edc3 100644 --- a/Application/DevLogInfra/Sources/Service/FunctionAPIClient.swift +++ b/Application/DevLogInfra/Sources/Service/FunctionAPIClient.swift @@ -10,8 +10,24 @@ import Foundation import DevLogData import Nexa -struct FunctionAPIClient { - private let authTokenProvider = FirebaseAuthTokenProvider() +final class FunctionAPIClient { + static let shared = FunctionAPIClient() + + private let apiClient: Result + + private init() { + let authTokenProvider = FirebaseAuthTokenProvider() + apiClient = Result { + try NXAPIClient( + configuration: NXClientConfiguration( + baseURL: FirebaseConfiguration.functionAPIBaseURL(), + headers: ["Accept": "application/json"], + serverErrorDecoder: FunctionAPIServerErrorDecoder(), + authTokenProvider: authTokenProvider + ) + ) + } + } func send( _ endpoint: FunctionAPIEndpoint, @@ -71,14 +87,7 @@ struct FunctionAPIClient { } private func client() throws -> NXAPIClient { - try NXAPIClient( - configuration: NXClientConfiguration( - baseURL: FirebaseConfiguration.functionAPIBaseURL(), - headers: ["Accept": "application/json"], - serverErrorDecoder: FunctionAPIServerErrorDecoder(), - authTokenProvider: authTokenProvider - ) - ) + try apiClient.get() } } diff --git a/Application/DevLogInfra/Sources/Service/PushNotificationServiceImpl.swift b/Application/DevLogInfra/Sources/Service/PushNotificationServiceImpl.swift index 7ac10ca7..cd9029af 100644 --- a/Application/DevLogInfra/Sources/Service/PushNotificationServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/PushNotificationServiceImpl.swift @@ -224,7 +224,7 @@ final class PushNotificationServiceImpl: PushNotificationService { do { guard Auth.auth().currentUser?.uid != nil else { throw DataLayerError.notAuthenticated } - try await FunctionAPIClient().send( + try await FunctionAPIClient.shared.send( .requestPushNotificationDeletion(notificationID) ) } catch { @@ -238,7 +238,7 @@ final class PushNotificationServiceImpl: PushNotificationService { do { guard Auth.auth().currentUser?.uid != nil else { throw DataLayerError.notAuthenticated } - try await FunctionAPIClient().send( + try await FunctionAPIClient.shared.send( .undoPushNotificationDeletion(notificationID) ) } catch { diff --git a/Application/DevLogInfra/Sources/Service/SocialLogin/AppleAuthenticationServiceImpl.swift b/Application/DevLogInfra/Sources/Service/SocialLogin/AppleAuthenticationServiceImpl.swift index c40b30de..99eb9222 100644 --- a/Application/DevLogInfra/Sources/Service/SocialLogin/AppleAuthenticationServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/SocialLogin/AppleAuthenticationServiceImpl.swift @@ -266,7 +266,7 @@ final class AppleAuthenticationServiceImpl: AuthenticationService { throw URLError(.badServerResponse) } - let response = try await FunctionAPIClient().send( + let response = try await FunctionAPIClient.shared.send( .requestAppleCustomToken, payload: [ "idToken": idToken, @@ -283,7 +283,7 @@ final class AppleAuthenticationServiceImpl: AuthenticationService { // Apple AceessToken 재발급 메서드 private func refreshAppleAccessToken() async throws -> String { - let response = try await FunctionAPIClient().send(.refreshAppleAccessToken) + let response = try await FunctionAPIClient.shared.send(.refreshAppleAccessToken) guard let accessToken = response.token else { throw URLError(.cannotParseResponse) @@ -298,7 +298,7 @@ final class AppleAuthenticationServiceImpl: AuthenticationService { throw URLError(.userAuthenticationRequired) } - let response = try await FunctionAPIClient().send( + let response = try await FunctionAPIClient.shared.send( .requestAppleRefreshToken, payload: ["authorizationCode": authorizationCode] ) @@ -311,7 +311,7 @@ final class AppleAuthenticationServiceImpl: AuthenticationService { // Apple AccessToken 취소 메서드 func revokeAppleAccessToken(token: String) async throws { - try await FunctionAPIClient().send( + try await FunctionAPIClient.shared.send( .revokeAppleAccessToken, payload: ["token": token] ) diff --git a/Application/DevLogInfra/Sources/Service/SocialLogin/GithubAuthenticationServiceImpl.swift b/Application/DevLogInfra/Sources/Service/SocialLogin/GithubAuthenticationServiceImpl.swift index 0687e7bc..8a2aedfe 100644 --- a/Application/DevLogInfra/Sources/Service/SocialLogin/GithubAuthenticationServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/SocialLogin/GithubAuthenticationServiceImpl.swift @@ -245,7 +245,7 @@ final class GithubAuthenticationServiceImpl: NSObject, AuthenticationService { // Firebase Function 호출: Custom Token 발급 private func requestTokens(authorizationCode: String) async throws -> (String, String) { do { - let response = try await FunctionAPIClient().send( + let response = try await FunctionAPIClient.shared.send( .requestGithubTokens, payload: ["code": authorizationCode], requiresAuthentication: false @@ -268,7 +268,7 @@ final class GithubAuthenticationServiceImpl: NSObject, AuthenticationService { param["accessToken"] = accessToken } - try await FunctionAPIClient().send( + try await FunctionAPIClient.shared.send( .revokeGithubAccessToken, payload: param ) diff --git a/Application/DevLogInfra/Sources/Service/TodoServiceImpl.swift b/Application/DevLogInfra/Sources/Service/TodoServiceImpl.swift index 8c1a717e..931b4769 100644 --- a/Application/DevLogInfra/Sources/Service/TodoServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/TodoServiceImpl.swift @@ -215,7 +215,7 @@ final class TodoServiceImpl: TodoService { logger.info("Requesting todo deletion") do { - try await FunctionAPIClient().send( + try await FunctionAPIClient.shared.send( .requestTodoDeletion(todoId) ) @@ -233,7 +233,7 @@ final class TodoServiceImpl: TodoService { logger.info("Undoing todo deletion") do { - try await FunctionAPIClient().send( + try await FunctionAPIClient.shared.send( .undoTodoDeletion(todoId) ) diff --git a/Application/DevLogInfra/Sources/Service/WebPageServiceImpl.swift b/Application/DevLogInfra/Sources/Service/WebPageServiceImpl.swift index 3c4369df..f432aa8f 100644 --- a/Application/DevLogInfra/Sources/Service/WebPageServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/WebPageServiceImpl.swift @@ -91,7 +91,7 @@ final class WebPageServiceImpl: WebPageService { } do { - try await FunctionAPIClient().send( + try await FunctionAPIClient.shared.send( .requestWebPageDeletion(id) ) logger.info("Successfully requested web page deletion") @@ -111,7 +111,7 @@ final class WebPageServiceImpl: WebPageService { } do { - try await FunctionAPIClient().send( + try await FunctionAPIClient.shared.send( .undoWebPageDeletion(id) ) logger.info("Successfully undone web page deletion")