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")