From dfee5d579ccb5c8da833b0dd9768180263919053 Mon Sep 17 00:00:00 2001 From: Roy Date: Fri, 12 Dec 2025 12:42:41 +0900 Subject: [PATCH 1/4] =?UTF-8?q?[feat]:=20deeplink=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81=20firebase=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Router/DeeplinkDestination.swift | 27 +++ Domain/Sources/Router/DeeplinkRouter.swift | 97 ++++++++++ Features/Main/Sources/MainCoordinator.swift | 178 +++++++++--------- Tuist/Package.swift | 6 +- 4 files changed, 218 insertions(+), 90 deletions(-) create mode 100644 Domain/Sources/Router/DeeplinkDestination.swift create mode 100644 Domain/Sources/Router/DeeplinkRouter.swift diff --git a/Domain/Sources/Router/DeeplinkDestination.swift b/Domain/Sources/Router/DeeplinkDestination.swift new file mode 100644 index 0000000..41327cf --- /dev/null +++ b/Domain/Sources/Router/DeeplinkDestination.swift @@ -0,0 +1,27 @@ +// +// DeeplinkDestination.swift +// Domain +// +// Created by Wonji Suh on 12/12/25. +// + +import Foundation + +public enum DeeplinkDestination: Equatable, Sendable { + case travel(TravelDeeplink) + case invite(code: String) + case unknown(url: String) +} + +public enum TravelDeeplink: Equatable, Sendable { + case detail(travelId: String) + case settings(travelId: String) + case expense(travelId: String, expenseId: String) + case settlement(travelId: String) +} + +public enum DeeplinkResult: Equatable, Sendable { + case success(DeeplinkDestination) + case requiresLogin(destination: DeeplinkDestination) + case invalid(url: String, reason: String) +} diff --git a/Domain/Sources/Router/DeeplinkRouter.swift b/Domain/Sources/Router/DeeplinkRouter.swift new file mode 100644 index 0000000..d146f17 --- /dev/null +++ b/Domain/Sources/Router/DeeplinkRouter.swift @@ -0,0 +1,97 @@ +// +// DeeplinkRouter.swift +// Domain +// +// Created by Wonji Suh on 12/12/25. +// + +import Foundation +import Dependencies + +public struct DeeplinkRouter: Sendable { + + public init() {} + + // MARK: - Public Interface + + public func parse(_ urlString: String) -> DeeplinkResult { + guard let url = URL(string: urlString), + url.scheme == "sseudam" else { + return .invalid(url: urlString, reason: "Invalid scheme") + } + + let pathComponents = url.pathComponents.filter { $0 != "/" } + + switch url.host ?? pathComponents.first { + case "travel": + return parseTravelDeeplink(url: url, pathComponents: pathComponents) + case "invite": + return parseInviteDeeplink(url: url) + default: + return .success(.unknown(url: urlString)) + } + } + + // MARK: - Private Parsing + + private func parseTravelDeeplink(url: URL, pathComponents: [String]) -> DeeplinkResult { + guard let (travelId, remainingComponents) = extractTravelId(url: url, pathComponents: pathComponents), + !travelId.isEmpty else { + return .invalid(url: url.absoluteString, reason: "Missing travel ID") + } + + let travelDeeplink: TravelDeeplink = { + switch remainingComponents.first { + case "settings": + return .settings(travelId: travelId) + case "expense" where remainingComponents.count >= 2: + return .expense(travelId: travelId, expenseId: remainingComponents[1]) + case "settlement": + return .settlement(travelId: travelId) + default: + return .detail(travelId: travelId) + } + }() + + return .success(.travel(travelDeeplink)) + } + + private func parseInviteDeeplink(url: URL) -> DeeplinkResult { + guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false), + let inviteCode = components.queryItems?.first(where: { $0.name == "code" })?.value, + !inviteCode.isEmpty else { + return .invalid(url: url.absoluteString, reason: "Missing invite code") + } + + return .requiresLogin(destination: .invite(code: inviteCode)) + } + + private func extractTravelId(url: URL, pathComponents: [String]) -> (String, [String])? { + if pathComponents.first == "travel" && pathComponents.count >= 2 { + // ["travel", "123", "expense", "456"] + let travelId = pathComponents[1] + let remaining = Array(pathComponents.dropFirst(2)) + return (travelId, remaining) + } else if url.host == "travel" && pathComponents.count >= 1 { + // host="travel", path=["123", "expense", "456"] + let travelId = pathComponents[0] + let remaining = Array(pathComponents.dropFirst(1)) + return (travelId, remaining) + } + return nil + } +} + +// MARK: - Dependencies + +extension DeeplinkRouter: DependencyKey { + public static let liveValue = DeeplinkRouter() + public static let testValue = DeeplinkRouter() +} + +extension DependencyValues { + public var deeplinkRouter: DeeplinkRouter { + get { self[DeeplinkRouter.self] } + set { self[DeeplinkRouter.self] = newValue } + } +} diff --git a/Features/Main/Sources/MainCoordinator.swift b/Features/Main/Sources/MainCoordinator.swift index 63eda5f..a172443 100644 --- a/Features/Main/Sources/MainCoordinator.swift +++ b/Features/Main/Sources/MainCoordinator.swift @@ -11,6 +11,7 @@ import ComposableArchitecture import SettlementFeature import LogMacro import MemberFeature +import Domain @Reducer public struct MainCoordinator { @@ -37,6 +38,8 @@ public struct MainCoordinator { case presentLogin } + @Dependency(\.deeplinkRouter) var deeplinkRouter + public var body: some ReducerOf { Reduce { state, action in switch action { @@ -102,8 +105,7 @@ extension MainCoordinator { state.routes.push(.memberManage(.init(travelId: travelId))) return .none - case let .routeAction(_, .travelSetting(.delegate(.navigateToTravelDetail(travelId)))): - // 여행 수정 완료 후 해당 여행의 상세 페이지로 이동 + case .routeAction(_, .travelSetting(.delegate(.navigateToTravelDetail(_)))): return .routeWithDelaysIfUnsupported(state.routes, action: \.router) { $0.goBackTo(\.travelList) } @@ -150,121 +152,121 @@ extension MainCoordinator { } private func handlePushDeepLink(state: inout State, urlString: String) -> Effect { - guard let url = URL(string: urlString), - url.scheme == "sseudam" else { - #logDebug("❌ Invalid push deep link URL: \(urlString)") + #logDebug("🔗 Processing deeplink: \(urlString)") + + let result = deeplinkRouter.parse(urlString) + + switch result { + case .success(let destination): + return routeToDestination(state: &state, destination: destination) + case .requiresLogin(let destination): + return handleRequiresLogin(state: &state, destination: destination) + case .invalid(let url, let reason): + #logDebug("❌ Invalid deeplink: \(url) - \(reason)") return .none } + } - let pathComponents = url.pathComponents.filter { $0 != "/" } - #logDebug("🔗 Path components: \(pathComponents)") + // MARK: - DeepLink Routing - // URL 구조: sseudam://travel/123/expense/456 또는 sseudam://invite?code=123 - if url.host == "invite" || pathComponents.first == "invite" { - return handleInviteDeepLink(state: &state, url: url) - } else if url.host == "travel" || pathComponents.first == "travel" { - return handleTravelDeepLink(state: &state, url: url, pathComponents: pathComponents) - } else { - #logDebug("❌ Unknown deep link scheme. Host: \(url.host ?? "nil"), Path: \(pathComponents)") + private func routeToDestination( + state: inout State, + destination: DeeplinkDestination + ) -> Effect { + switch destination { + case .travel(let travelDeeplink): + return handleTravelDestination(state: &state, travelDeeplink: travelDeeplink) + case .invite(let code): + // 이미 로그인된 상태에서 초대 코드 처리 + return .send(.router(.routeAction(id: 0, action: .travelList(.openInviteCode(code))))) + case .unknown(let url): + #logDebug("🤷‍♂️ Unknown deeplink: \(url)") return .none } } - private func handleTravelDeepLink( + private func handleRequiresLogin( state: inout State, - url: URL, - pathComponents: [String] + destination: DeeplinkDestination ) -> Effect { - - var travelId: String - var remainingComponents: [String] - - // URL 구조 분석: sseudam://travel/123/expense/456 또는 sseudam://travel/{id}/... - if pathComponents.first == "travel" && pathComponents.count >= 2 { - // 표준 구조: ["travel", "123", "expense", "456"] - travelId = pathComponents[1] - remainingComponents = Array(pathComponents.dropFirst(2)) - } else if url.host == "travel" && pathComponents.count >= 1 { - // 일부 푸시 페이로드는 host에만 travel이 있고 path는 ["{id}", "expense", "{expenseId}"] 형태 - travelId = pathComponents[0] - remainingComponents = Array(pathComponents.dropFirst(1)) - } else { - #logDebug("❌ Invalid travel deep link structure: \(pathComponents)") + // 초대 코드의 경우 AppFeature로 전달하여 로그인 상태 체크 + // TODO: 필요시 다른 destination들도 처리 + guard case .invite(_) = destination else { + #logDebug("❌ Unhandled requiresLogin destination: \(destination)") return .none } + // AppFeature의 기존 로직을 사용하기 위해 다시 전달 + return .send(.delegate(.presentLogin)) + } - // settings 경로인 경우 바로 TravelSetting으로 이동 - if remainingComponents.count >= 1, remainingComponents[0] == "settings" { - #logDebug("⚙️ Navigating to travel settings") - // 기존 여행 관련 화면들 정리 - if let settlementIndex = state.routes.lastIndex(where: { - if case .settlementCoordinator = $0.screen { return true } - return false - }) { - state.routes.removeSubrange(settlementIndex...) - } - if let travelSettingIndex = state.routes.lastIndex(where: { - if case .travelSetting = $0.screen { return true } - return false - }) { - state.routes.removeSubrange(travelSettingIndex...) - } - // 여행 설정 페이지로 직접 이동 - state.routes.push(.travelSetting(.init(travelId: travelId))) - return .none + private func handleTravelDestination( + state: inout State, + travelDeeplink: TravelDeeplink + ) -> Effect { + switch travelDeeplink { + case .detail(let travelId): + return navigateToTravelDetail(state: &state, travelId: travelId) + case .settings(let travelId): + return navigateToTravelSettings(state: &state, travelId: travelId) + case .expense(let travelId, let expenseId): + return navigateToExpenseDetail(state: &state, travelId: travelId, expenseId: expenseId) + case .settlement(let travelId): + return navigateToSettlementTab(state: &state, travelId: travelId) } + } + + private func navigateToTravelSettings(state: inout State, travelId: String) -> Effect { + #logDebug("⚙️ Navigating to travel settings") + clearTravelRelatedScreens(state: &state) + state.routes.push(.travelSetting(.init(travelId: travelId))) + return .none + } - // 일반적인 여행 상세 페이지 처리 + private func navigateToTravelDetail(state: inout State, travelId: String) -> Effect { + #logDebug("🏝️ Navigating to travel detail") let currentTravelId = getCurrentTravelId(from: state) if currentTravelId != travelId { - // 다른 여행이거나 여행 화면이 없으면 새로 열기 - if let settlementIndex = state.routes.lastIndex(where: { - if case .settlementCoordinator = $0.screen { return true } - return false - }) { - // 기존 여행 화면 제거하고 새로운 여행 화면 추가 - state.routes.removeSubrange(settlementIndex...) - } + clearSettlementScreens(state: &state) state.routes.push(.settlementCoordinator(.init(travelId: travelId))) } + return .none + } - // 추가 네비게이션 처리 - if remainingComponents.count >= 2, remainingComponents[0] == "expense" { - let expenseId = remainingComponents[1] - #logDebug("💰 Navigating to expense detail: \(expenseId)") + private func navigateToExpenseDetail(state: inout State, travelId: String, expenseId: String) -> Effect { + #logDebug("💰 Navigating to expense detail: \(expenseId)") + _ = navigateToTravelDetail(state: &state, travelId: travelId) + let routeIndex = state.routes.count - 1 + return .send(.router(.routeAction(id: routeIndex, action: .settlementCoordinator(.navigateToExpenseTab(expenseId))))) + } - // 지출 목록 탭으로 이동하고 특정 지출을 찾아서 표시 - let routeIndex = state.routes.count - 1 - return .send(.router(.routeAction(id: routeIndex, action: .settlementCoordinator(.navigateToExpenseTab(expenseId))))) + private func navigateToSettlementTab(state: inout State, travelId: String) -> Effect { + #logDebug("📊 Navigating to settlement tab") + _ = navigateToTravelDetail(state: &state, travelId: travelId) + let routeIndex = state.routes.count - 1 + return .send(.router(.routeAction(id: routeIndex, action: .settlementCoordinator(.navigateToSettlementTab)))) + } - } else if remainingComponents.count >= 1, remainingComponents[0] == "settlement" { - #logDebug("📊 Navigating to settlement tab") - // 정산 탭으로 이동 - let routeIndex = state.routes.count - 1 - return .send(.router(.routeAction(id: routeIndex, action: .settlementCoordinator(.navigateToSettlementTab)))) + private func clearTravelRelatedScreens(state: inout State) { + clearSettlementScreens(state: &state) + if let travelSettingIndex = state.routes.lastIndex(where: { + if case .travelSetting = $0.screen { return true } + return false + }) { + state.routes.removeSubrange(travelSettingIndex...) } - - #logDebug("🏝️ Navigating to travel detail only") - return .none } - private func handleInviteDeepLink( - state: inout State, - url: URL - ) -> Effect { - guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false), - let inviteCode = components.queryItems?.first(where: { $0.name == "code" })?.value else { - print("❌ Invalid invite deep link: missing code") - return .none + private func clearSettlementScreens(state: inout State) { + if let settlementIndex = state.routes.lastIndex(where: { + if case .settlementCoordinator = $0.screen { return true } + return false + }) { + state.routes.removeSubrange(settlementIndex...) } - - #logDebug("🎫 Processing invite code: \(inviteCode)") - - // 초대 코드 처리를 위해 TravelListFeature로 전달 - return .send(.router(.routeAction(id: 0, action: .travelList(.openInviteCode(inviteCode))))) } + private func getCurrentTravelId(from state: State) -> String? { // 현재 열려있는 SettlementCoordinator에서 travelId 추출 for route in state.routes.reversed() { diff --git a/Tuist/Package.swift b/Tuist/Package.swift index fb6cffd..f6f9c7e 100644 --- a/Tuist/Package.swift +++ b/Tuist/Package.swift @@ -10,7 +10,9 @@ let packageSettings = PackageSettings( "TCACoordinators": .framework, "Moya": .framework, "LogMacro": .framework, - + "GoogleSignIn": .framework, + "AppAuth": .framework, + "GTMAppAuth": .framework ] ) #endif @@ -25,6 +27,6 @@ let package = Package( .package(url: "https://github.com/google/GoogleSignIn-iOS.git", from: "9.0.0"), .package(url: "https://github.com/Roy-wonji/LogMacro.git", from: "1.1.1"), .package(url: "https://github.com/Moya/Moya.git", exact: "15.0.3"), - .package(url: "https://github.com/firebase/firebase-ios-sdk.git", exact: "11.0.0") + .package(url: "https://github.com/firebase/firebase-ios-sdk.git", exact: "12.7.0") ] ) From c290436853bb5b12cbe2b7c10ac11c4540ebd24b Mon Sep 17 00:00:00 2001 From: Roy Date: Fri, 12 Dec 2025 13:54:59 +0900 Subject: [PATCH 2/4] [chore] deeplink fix --- Features/Main/Sources/MainCoordinator.swift | 30 ++++++++++++++------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/Features/Main/Sources/MainCoordinator.swift b/Features/Main/Sources/MainCoordinator.swift index a172443..a11245b 100644 --- a/Features/Main/Sources/MainCoordinator.swift +++ b/Features/Main/Sources/MainCoordinator.swift @@ -225,25 +225,19 @@ extension MainCoordinator { private func navigateToTravelDetail(state: inout State, travelId: String) -> Effect { #logDebug("🏝️ Navigating to travel detail") - let currentTravelId = getCurrentTravelId(from: state) - if currentTravelId != travelId { - clearSettlementScreens(state: &state) - state.routes.push(.settlementCoordinator(.init(travelId: travelId))) - } + _ = ensureSettlementCoordinatorRoute(state: &state, travelId: travelId) return .none } private func navigateToExpenseDetail(state: inout State, travelId: String, expenseId: String) -> Effect { #logDebug("💰 Navigating to expense detail: \(expenseId)") - _ = navigateToTravelDetail(state: &state, travelId: travelId) - let routeIndex = state.routes.count - 1 + let routeIndex = ensureSettlementCoordinatorRoute(state: &state, travelId: travelId) return .send(.router(.routeAction(id: routeIndex, action: .settlementCoordinator(.navigateToExpenseTab(expenseId))))) } private func navigateToSettlementTab(state: inout State, travelId: String) -> Effect { #logDebug("📊 Navigating to settlement tab") - _ = navigateToTravelDetail(state: &state, travelId: travelId) - let routeIndex = state.routes.count - 1 + let routeIndex = ensureSettlementCoordinatorRoute(state: &state, travelId: travelId) return .send(.router(.routeAction(id: routeIndex, action: .settlementCoordinator(.navigateToSettlementTab)))) } @@ -257,6 +251,24 @@ extension MainCoordinator { } } + private func ensureSettlementCoordinatorRoute(state: inout State, travelId: String) -> Int { + if let existingIndex = state.routes.lastIndex(where: { + if case let .settlementCoordinator(settlementState) = $0.screen { + return settlementState.travelId == travelId + } + return false + }) { + if existingIndex < state.routes.count - 1 { + state.routes.removeSubrange((existingIndex + 1)...) + } + return existingIndex + } + + clearSettlementScreens(state: &state) + state.routes.push(.settlementCoordinator(.init(travelId: travelId))) + return state.routes.count - 1 + } + private func clearSettlementScreens(state: inout State) { if let settlementIndex = state.routes.lastIndex(where: { if case .settlementCoordinator = $0.screen { return true } From d1e7844a8046c805bac668cb0ce7b11a87c3d46e Mon Sep 17 00:00:00 2001 From: Roy Date: Fri, 12 Dec 2025 13:55:44 +0900 Subject: [PATCH 3/4] [chore] deeplink fix --- Data/Project.swift | 1 + Tuist/Package.swift | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Data/Project.swift b/Data/Project.swift index 3e35c5c..9e3149d 100644 --- a/Data/Project.swift +++ b/Data/Project.swift @@ -8,6 +8,7 @@ let project = Project.makeFramework( .NetworkService, .SPM.Supabase, .SPM.GoogleSignIn, + .SPM.AppAuth, .SPM.FirebaseAnalytics, .SPM.FirebaseCrashlytics, ], diff --git a/Tuist/Package.swift b/Tuist/Package.swift index f6f9c7e..b2678dc 100644 --- a/Tuist/Package.swift +++ b/Tuist/Package.swift @@ -10,9 +10,12 @@ let packageSettings = PackageSettings( "TCACoordinators": .framework, "Moya": .framework, "LogMacro": .framework, - "GoogleSignIn": .framework, - "AppAuth": .framework, - "GTMAppAuth": .framework +// "FirebaseCore": .staticLibrary, +// "FirebaseAuth": .staticLibrary, +// "FirebaseFirestore": .staticLibrary, +// "FirebaseAnalytics": .staticLibrary, +// "FirebaseCrashlytics": .staticLibrary, +// "FirebaseRemoteConfig": .staticLibrary ] ) #endif From 5fb85bb4fbb687d2b3023302fb2a518c026c5466 Mon Sep 17 00:00:00 2001 From: Roy Date: Fri, 12 Dec 2025 16:46:43 +0900 Subject: [PATCH 4/4] [chore] comment out Firebase analytics dependencies --- Data/Project.swift | 6 +- .../Analytics/FirebaseAnalyticsManager.swift | 170 ++++++++-------- .../AnalyticsRepositoryProtocol.swift | 12 +- .../Analytics/MockAnalyticsRepository.swift | 188 +++++++++--------- .../UseCase/Analytics/AnalyticsUseCase.swift | 62 +++--- .../Analytics/AnalyticsUseCaseProtocol.swift | 12 +- .../Sources/Login/Reducer/LoginFeature.swift | 8 +- .../Sources/Reducer/MemberManageFeature.swift | 6 +- .../Sources/Reducer/BasicSettingFeature.swift | 4 +- .../Sources/Reducer/TravelManageFeature.swift | 6 +- .../Sources/Application/AppDelegate.swift | 8 +- .../Application/LiveDependencies.swift | 2 +- SseuDamApp/Sources/Reducer/AppFeature.swift | 4 +- Tuist/Package.swift | 3 +- 14 files changed, 246 insertions(+), 245 deletions(-) diff --git a/Data/Project.swift b/Data/Project.swift index 9e3149d..0a82a83 100644 --- a/Data/Project.swift +++ b/Data/Project.swift @@ -9,12 +9,12 @@ let project = Project.makeFramework( .SPM.Supabase, .SPM.GoogleSignIn, .SPM.AppAuth, - .SPM.FirebaseAnalytics, - .SPM.FirebaseCrashlytics, +// .SPM.FirebaseAnalytics, +// .SPM.FirebaseCrashlytics, ], hasTests: true, settings: .settings( base: SettingsDictionary() - .otherLinkerFlags(["-all_load", "-ObjC"]), +// .otherLinkerFlags(["-all_load", "-ObjC"]), ) ) diff --git a/Data/Sources/Analytics/FirebaseAnalyticsManager.swift b/Data/Sources/Analytics/FirebaseAnalyticsManager.swift index 1669343..c73d585 100644 --- a/Data/Sources/Analytics/FirebaseAnalyticsManager.swift +++ b/Data/Sources/Analytics/FirebaseAnalyticsManager.swift @@ -1,85 +1,85 @@ -import Foundation -import FirebaseAnalytics -import Domain -import LogMacro - -/// FirebaseAnalytics를 사용한 Analytics Repository 구현체 -public class FirebaseAnalyticsRepository: AnalyticsRepositoryProtocol, @unchecked Sendable { - - public init() { - #logDebug("🔥 [Analytics] ===== FIREBASE ANALYTICS REPOSITORY INITIALIZED =====") - #logDebug("🔥 [Analytics] Sending app_analytics_initialized event...") - - Analytics.logEvent("app_analytics_initialized", parameters: [ - "timestamp": Date().timeIntervalSince1970, - "version": "1.0" - ]) - } - - public func sendEvent(_ event: AnalyticsEvent) async { - switch event { - case .auth(let eventType, let data): - await sendAuthEvent(eventType, data) - case .deeplink(let data): - await sendDeeplinkEvent(data) - case .travel(let eventType, let data): - await sendTravelEvent(eventType, data) - case .expense(let eventType, let data): - await sendExpenseEvent(eventType, data) - } - } - - // MARK: - Private Event Handlers - - private func sendAuthEvent(_ eventType: AuthEventType, _ data: AuthEventData) async { - var params: [String: Any] = ["social_type": data.socialType] - if let isFirst = data.isFirst { - params["is_first"] = isFirst - } - - #logDebug("🔥 [Analytics] Parameters: \(params)") - Analytics.logEvent(eventType.rawValue, parameters: params) - } - - private func sendDeeplinkEvent(_ data: DeeplinkEventData) async { - let parameters: [String: Any] = [ - "deeplink": data.deeplink, - "deeplink_type": data.type - ] - - #logDebug("🔥 [Analytics] Parameters: \(parameters)") - Analytics.logEvent("deeplink_open", parameters: parameters) - #logDebug("🔥 [Analytics] ✅ deeplink_open event sent to Firebase") - } - - private func sendTravelEvent(_ eventType: TravelEventType, _ data: TravelEventData) async { - var params: [String: Any] = ["travel_id": data.travelId] - - // Optional fields based on event type - if let userId = data.userId { params["user_id"] = userId } - if let memberId = data.memberId { params["member_id"] = memberId } - if let role = data.role { params["role"] = role } - if let newOwnerId = data.newOwnerId { params["new_owner_id"] = newOwnerId } - - #logDebug("🔥 [Analytics] Parameters: \(params)") - Analytics.logEvent(eventType.rawValue, parameters: params) - } - - private func sendExpenseEvent(_ eventType: ExpenseEventType, _ data: ExpenseEventData) async { - var params: [String: Any] = ["travel_id": data.travelId] - - // Add optional fields - if let expenseId = data.expenseId { params["expense_id"] = expenseId } - if let amount = data.amount { params["amount"] = amount } - if let currency = data.currency { params["currency"] = currency } - if let category = data.category { params["category"] = category } - if let payerId = data.payerId { params["payer_id"] = payerId } - if let source = data.source { params["source"] = source } - if let tab = data.tab { params["tab"] = tab } - if let expenseDate = data.expenseDate { params["expense_date"] = expenseDate } - if let errorCode = data.errorCode { params["error_code"] = errorCode } - - #logDebug("🔥 [Analytics] Parameters: \(params)") - Analytics.logEvent(eventType.rawValue, parameters: params) - } -} \ No newline at end of file +//import Foundation +//import FirebaseAnalytics +//import Domain +//import LogMacro +// +///// FirebaseAnalytics를 사용한 Analytics Repository 구현체 +//public class FirebaseAnalyticsRepository: AnalyticsRepositoryProtocol, @unchecked Sendable { +// +// public init() { +// #logDebug("🔥 [Analytics] ===== FIREBASE ANALYTICS REPOSITORY INITIALIZED =====") +// #logDebug("🔥 [Analytics] Sending app_analytics_initialized event...") +// +// Analytics.logEvent("app_analytics_initialized", parameters: [ +// "timestamp": Date().timeIntervalSince1970, +// "version": "1.0" +// ]) +// } +// +// public func sendEvent(_ event: AnalyticsEvent) async { +// switch event { +// case .auth(let eventType, let data): +// await sendAuthEvent(eventType, data) +// case .deeplink(let data): +// await sendDeeplinkEvent(data) +// case .travel(let eventType, let data): +// await sendTravelEvent(eventType, data) +// case .expense(let eventType, let data): +// await sendExpenseEvent(eventType, data) +// } +// } +// +// // MARK: - Private Event Handlers +// +// private func sendAuthEvent(_ eventType: AuthEventType, _ data: AuthEventData) async { +// var params: [String: Any] = ["social_type": data.socialType] +// if let isFirst = data.isFirst { +// params["is_first"] = isFirst +// } +// +// #logDebug("🔥 [Analytics] Parameters: \(params)") +// Analytics.logEvent(eventType.rawValue, parameters: params) +// } +// +// private func sendDeeplinkEvent(_ data: DeeplinkEventData) async { +// let parameters: [String: Any] = [ +// "deeplink": data.deeplink, +// "deeplink_type": data.type +// ] +// +// #logDebug("🔥 [Analytics] Parameters: \(parameters)") +// Analytics.logEvent("deeplink_open", parameters: parameters) +// #logDebug("🔥 [Analytics] ✅ deeplink_open event sent to Firebase") +// } +// +// private func sendTravelEvent(_ eventType: TravelEventType, _ data: TravelEventData) async { +// var params: [String: Any] = ["travel_id": data.travelId] +// +// // Optional fields based on event type +// if let userId = data.userId { params["user_id"] = userId } +// if let memberId = data.memberId { params["member_id"] = memberId } +// if let role = data.role { params["role"] = role } +// if let newOwnerId = data.newOwnerId { params["new_owner_id"] = newOwnerId } +// +// #logDebug("🔥 [Analytics] Parameters: \(params)") +// Analytics.logEvent(eventType.rawValue, parameters: params) +// } +// +// private func sendExpenseEvent(_ eventType: ExpenseEventType, _ data: ExpenseEventData) async { +// var params: [String: Any] = ["travel_id": data.travelId] +// +// // Add optional fields +// if let expenseId = data.expenseId { params["expense_id"] = expenseId } +// if let amount = data.amount { params["amount"] = amount } +// if let currency = data.currency { params["currency"] = currency } +// if let category = data.category { params["category"] = category } +// if let payerId = data.payerId { params["payer_id"] = payerId } +// if let source = data.source { params["source"] = source } +// if let tab = data.tab { params["tab"] = tab } +// if let expenseDate = data.expenseDate { params["expense_date"] = expenseDate } +// if let errorCode = data.errorCode { params["error_code"] = errorCode } +// +// #logDebug("🔥 [Analytics] Parameters: \(params)") +// Analytics.logEvent(eventType.rawValue, parameters: params) +// } +//} diff --git a/Domain/Sources/Repository/Analytics/AnalyticsRepositoryProtocol.swift b/Domain/Sources/Repository/Analytics/AnalyticsRepositoryProtocol.swift index 5374ba1..d714cdc 100644 --- a/Domain/Sources/Repository/Analytics/AnalyticsRepositoryProtocol.swift +++ b/Domain/Sources/Repository/Analytics/AnalyticsRepositoryProtocol.swift @@ -1,6 +1,6 @@ -import Foundation - -/// Analytics 이벤트 전송을 위한 Repository 프로토콜 -public protocol AnalyticsRepositoryProtocol: Sendable { - func sendEvent(_ event: AnalyticsEvent) async -} \ No newline at end of file +//import Foundation +// +///// Analytics 이벤트 전송을 위한 Repository 프로토콜 +//public protocol AnalyticsRepositoryProtocol: Sendable { +// func sendEvent(_ event: AnalyticsEvent) async +//} diff --git a/Domain/Sources/Repository/Analytics/MockAnalyticsRepository.swift b/Domain/Sources/Repository/Analytics/MockAnalyticsRepository.swift index 1d8a988..acb40f4 100644 --- a/Domain/Sources/Repository/Analytics/MockAnalyticsRepository.swift +++ b/Domain/Sources/Repository/Analytics/MockAnalyticsRepository.swift @@ -1,94 +1,94 @@ -import Foundation - -/// Mock Analytics Repository for testing -public actor MockAnalyticsRepository: AnalyticsRepositoryProtocol, Sendable { - - // MARK: - Tracking - - private let eventsStorage = ThreadSafeContainer<[AnalyticsEvent]>([]) - - /// 추적된 이벤트들 (테스트에서 확인용) - public var trackedEvents: [AnalyticsEvent] { - eventsStorage.value - } - - public init() {} - - public func sendEvent(_ event: AnalyticsEvent) async { - eventsStorage.modify { $0.append(event) } - } - - /// 추적된 이벤트 초기화 (테스트 간 클린업용) - public func clearTrackedEvents() { - eventsStorage.modify { $0.removeAll() } - } - - /// 특정 타입의 이벤트가 추적되었는지 확인 - public func hasTrackedEvent(type: T.Type) -> Bool { - return trackedEvents.contains { event in - switch event { - case .auth: - return T.self == AuthEventType.self - case .travel: - return T.self == TravelEventType.self - case .expense: - return T.self == ExpenseEventType.self - case .deeplink: - return T.self == DeeplinkEventData.self - } - } - } - - /// Auth 이벤트 추적 개수 - public var authEventCount: Int { - trackedEvents.filter { - if case .auth = $0 { return true } - return false - }.count - } - - /// Travel 이벤트 추적 개수 - public var travelEventCount: Int { - trackedEvents.filter { - if case .travel = $0 { return true } - return false - }.count - } - - /// Expense 이벤트 추적 개수 - public var expenseEventCount: Int { - trackedEvents.filter { - if case .expense = $0 { return true } - return false - }.count - } - - /// Deeplink 이벤트 추적 개수 - public var deeplinkEventCount: Int { - trackedEvents.filter { - if case .deeplink = $0 { return true } - return false - }.count - } -} - -// MARK: - Thread Safety Helper - -private class ThreadSafeContainer: @unchecked Sendable { - private var storage: T - private let lock = NSLock() - - var value: T { - lock.withLock { storage } - } - - init(_ value: T) { - self.storage = value - } - - func modify(_ transform: (inout T) -> Void) { - lock.withLock { - transform(&storage) - } - } -} +//import Foundation +// +///// Mock Analytics Repository for testing +//public actor MockAnalyticsRepository: AnalyticsRepositoryProtocol, Sendable { +// +// // MARK: - Tracking +// +// private let eventsStorage = ThreadSafeContainer<[AnalyticsEvent]>([]) +// +// /// 추적된 이벤트들 (테스트에서 확인용) +// public var trackedEvents: [AnalyticsEvent] { +// eventsStorage.value +// } +// +// public init() {} +// +// public func sendEvent(_ event: AnalyticsEvent) async { +// eventsStorage.modify { $0.append(event) } +// } +// +// /// 추적된 이벤트 초기화 (테스트 간 클린업용) +// public func clearTrackedEvents() { +// eventsStorage.modify { $0.removeAll() } +// } +// +// /// 특정 타입의 이벤트가 추적되었는지 확인 +// public func hasTrackedEvent(type: T.Type) -> Bool { +// return trackedEvents.contains { event in +// switch event { +// case .auth: +// return T.self == AuthEventType.self +// case .travel: +// return T.self == TravelEventType.self +// case .expense: +// return T.self == ExpenseEventType.self +// case .deeplink: +// return T.self == DeeplinkEventData.self +// } +// } +// } +// +// /// Auth 이벤트 추적 개수 +// public var authEventCount: Int { +// trackedEvents.filter { +// if case .auth = $0 { return true } +// return false +// }.count +// } +// +// /// Travel 이벤트 추적 개수 +// public var travelEventCount: Int { +// trackedEvents.filter { +// if case .travel = $0 { return true } +// return false +// }.count +// } +// +// /// Expense 이벤트 추적 개수 +// public var expenseEventCount: Int { +// trackedEvents.filter { +// if case .expense = $0 { return true } +// return false +// }.count +// } +// +// /// Deeplink 이벤트 추적 개수 +// public var deeplinkEventCount: Int { +// trackedEvents.filter { +// if case .deeplink = $0 { return true } +// return false +// }.count +// } +//} +// +//// MARK: - Thread Safety Helper +// +//private class ThreadSafeContainer: @unchecked Sendable { +// private var storage: T +// private let lock = NSLock() +// +// var value: T { +// lock.withLock { storage } +// } +// +// init(_ value: T) { +// self.storage = value +// } +// +// func modify(_ transform: (inout T) -> Void) { +// lock.withLock { +// transform(&storage) +// } +// } +//} diff --git a/Domain/Sources/UseCase/Analytics/AnalyticsUseCase.swift b/Domain/Sources/UseCase/Analytics/AnalyticsUseCase.swift index abf9bee..0e5fe8f 100644 --- a/Domain/Sources/UseCase/Analytics/AnalyticsUseCase.swift +++ b/Domain/Sources/UseCase/Analytics/AnalyticsUseCase.swift @@ -1,31 +1,31 @@ -import Foundation -import ComposableArchitecture - -/// Analytics UseCase 구현체 -public struct AnalyticsUseCase: AnalyticsUseCaseProtocol, Sendable { - private let repository: any AnalyticsRepositoryProtocol - - public init(repository: any AnalyticsRepositoryProtocol) { - self.repository = repository - } - - public func track(_ event: AnalyticsEvent) { - Task { - await repository.sendEvent(event) - } - } -} - -// MARK: - Dependency Key - -extension AnalyticsUseCase: DependencyKey { - public static let liveValue: any AnalyticsUseCaseProtocol = AnalyticsUseCase(repository: MockAnalyticsRepository()) - public static let testValue: any AnalyticsUseCaseProtocol = AnalyticsUseCase(repository: MockAnalyticsRepository()) -} - -public extension DependencyValues { - var analyticsUseCase: any AnalyticsUseCaseProtocol { - get { self[AnalyticsUseCase.self] } - set { self[AnalyticsUseCase.self] = newValue } - } -} +//import Foundation +//import ComposableArchitecture +// +///// Analytics UseCase 구현체 +//public struct AnalyticsUseCase: AnalyticsUseCaseProtocol, Sendable { +// private let repository: any AnalyticsRepositoryProtocol +// +// public init(repository: any AnalyticsRepositoryProtocol) { +// self.repository = repository +// } +// +// public func track(_ event: AnalyticsEvent) { +// Task { +// await repository.sendEvent(event) +// } +// } +//} +// +//// MARK: - Dependency Key +// +//extension AnalyticsUseCase: DependencyKey { +// public static let liveValue: any AnalyticsUseCaseProtocol = AnalyticsUseCase(repository: MockAnalyticsRepository()) +// public static let testValue: any AnalyticsUseCaseProtocol = AnalyticsUseCase(repository: MockAnalyticsRepository()) +//} +// +//public extension DependencyValues { +// var analyticsUseCase: any AnalyticsUseCaseProtocol { +// get { self[AnalyticsUseCase.self] } +// set { self[AnalyticsUseCase.self] = newValue } +// } +//} diff --git a/Domain/Sources/UseCase/Analytics/AnalyticsUseCaseProtocol.swift b/Domain/Sources/UseCase/Analytics/AnalyticsUseCaseProtocol.swift index 249aac9..1a3528b 100644 --- a/Domain/Sources/UseCase/Analytics/AnalyticsUseCaseProtocol.swift +++ b/Domain/Sources/UseCase/Analytics/AnalyticsUseCaseProtocol.swift @@ -4,9 +4,9 @@ // // Created by Wonji Suh on 12/12/25. // - -import Foundation - -public protocol AnalyticsUseCaseProtocol: Sendable { - func track(_ event: AnalyticsEvent) -} +// +//import Foundation +// +//public protocol AnalyticsUseCaseProtocol: Sendable { +// func track(_ event: AnalyticsEvent) +//} diff --git a/Features/Login/Sources/Login/Reducer/LoginFeature.swift b/Features/Login/Sources/Login/Reducer/LoginFeature.swift index f4b4a80..225e8fe 100644 --- a/Features/Login/Sources/Login/Reducer/LoginFeature.swift +++ b/Features/Login/Sources/Login/Reducer/LoginFeature.swift @@ -84,7 +84,7 @@ public struct LoginFeature { @Dependency(UnifiedOAuthUseCase.self) var unifiedOAuthUseCase @Dependency(SessionUseCase.self) var sessionUseCase - @Dependency(\.analyticsUseCase) var analyticsUseCase +// @Dependency(\.analyticsUseCase) var analyticsUseCase nonisolated enum CancelID: Hashable { @@ -206,10 +206,10 @@ extension LoginFeature { // Analytics: 로그인/회원가입 구분 전송 let social = authEntity.provider.rawValue if case .signUpSuccess = outcome { - analyticsUseCase.track(.auth(.signupSuccess, AuthEventData(socialType: social))) - analyticsUseCase.track(.auth(.loginSuccess, AuthEventData(socialType: social, isFirst: true))) +// analyticsUseCase.track(.auth(.signupSuccess, AuthEventData(socialType: social))) +// analyticsUseCase.track(.auth(.loginSuccess, AuthEventData(socialType: social, isFirst: true))) } else { - analyticsUseCase.track(.auth(.loginSuccess, AuthEventData(socialType: social, isFirst: false))) +// analyticsUseCase.track(.auth(.loginSuccess, AuthEventData(socialType: social, isFirst: false))) } return .send(.delegate(.presentTravelList)) diff --git a/Features/Member/Sources/Reducer/MemberManageFeature.swift b/Features/Member/Sources/Reducer/MemberManageFeature.swift index 6edac5a..17f4bc3 100644 --- a/Features/Member/Sources/Reducer/MemberManageFeature.swift +++ b/Features/Member/Sources/Reducer/MemberManageFeature.swift @@ -67,7 +67,7 @@ public struct MemberManageFeature { @Dependency(\.fetchMemberUseCase) var fetchMemberUseCase @Dependency(\.deleteTravelMemberUseCase) var deleteTravelMemberUseCase @Dependency(\.delegateOwnerUseCase) var delegateOwnerUseCase - @Dependency(\.analyticsUseCase) var analyticsUseCase +// @Dependency(\.analyticsUseCase) var analyticsUseCase public var body: some Reducer { Reduce { state, action in @@ -132,7 +132,7 @@ public struct MemberManageFeature { case let .deleteMemberResponse(.success(memberId)): state.members.removeAll { $0.id == memberId } - analyticsUseCase.track(.travel(.memberLeave, TravelEventData(travelId: state.travelId, memberId: memberId))) +// analyticsUseCase.track(.travel(.memberLeave, TravelEventData(travelId: state.travelId, memberId: memberId))) return .none case .deleteMemberResponse(.failure): @@ -191,7 +191,7 @@ public struct MemberManageFeature { // 새 관리자 ID 찾기 if let newOwnerId = travel.members.first(where: { $0.role == .owner })?.id { - analyticsUseCase.track(.travel(.ownerDelegate, TravelEventData(travelId: state.travelId, newOwnerId: newOwnerId))) +// analyticsUseCase.track(.travel(.ownerDelegate, TravelEventData(travelId: state.travelId, newOwnerId: newOwnerId))) } return .send(.delegate(.finish)) diff --git a/Features/Travel/Sources/Reducer/BasicSettingFeature.swift b/Features/Travel/Sources/Reducer/BasicSettingFeature.swift index 88b8381..e3ca0de 100644 --- a/Features/Travel/Sources/Reducer/BasicSettingFeature.swift +++ b/Features/Travel/Sources/Reducer/BasicSettingFeature.swift @@ -112,7 +112,7 @@ public struct BasicSettingFeature { @Dependency(\.fetchCountriesUseCase) var fetchCountriesUseCase @Dependency(\.fetchExchangeRateUseCase) var fetchExchangeRateUseCase @Dependency(\.updateTravelUseCase) var updateTravelUseCase - @Dependency(\.analyticsUseCase) var analyticsUseCase +// @Dependency(\.analyticsUseCase) var analyticsUseCase public var body: some Reducer { BindingReducer() @@ -283,7 +283,7 @@ public struct BasicSettingFeature { state.selectedCurrency = updated.baseCurrency state.exchangeRate = String(updated.baseExchangeRate) - analyticsUseCase.track(.travel(.update, TravelEventData(travelId: updated.id))) +// analyticsUseCase.track(.travel(.update, TravelEventData(travelId: updated.id))) return .merge( .send(.updated(state.travel)), diff --git a/Features/Travel/Sources/Reducer/TravelManageFeature.swift b/Features/Travel/Sources/Reducer/TravelManageFeature.swift index 9c1ccf7..25b5fd9 100644 --- a/Features/Travel/Sources/Reducer/TravelManageFeature.swift +++ b/Features/Travel/Sources/Reducer/TravelManageFeature.swift @@ -49,7 +49,7 @@ public struct TravelManageFeature { @Dependency(\.leaveTravelUseCase) var leaveTravelUseCase @Dependency(\.deleteTravelUseCase) var deleteTravelUseCase - @Dependency(\.analyticsUseCase) var analyticsUseCase +// @Dependency(\.analyticsUseCase) var analyticsUseCase public var body: some Reducer { Reduce { state, action in @@ -80,7 +80,7 @@ public struct TravelManageFeature { case .leaveResponse(.success): state.isSubmitting = false - analyticsUseCase.track(.travel(.leave, TravelEventData(travelId: state.travelId, userId: nil))) +// analyticsUseCase.track(.travel(.leave, TravelEventData(travelId: state.travelId, userId: nil))) return .send(.dismissRequested) case .leaveResponse(.failure(let err)): @@ -106,7 +106,7 @@ public struct TravelManageFeature { case .deleteResponse(.success): state.isSubmitting = false - analyticsUseCase.track(.travel(.delete, TravelEventData(travelId: state.travelId))) +// analyticsUseCase.track(.travel(.delete, TravelEventData(travelId: state.travelId))) return .send(.dismissRequested) case .deleteResponse(.failure(let err)): diff --git a/SseuDamApp/Sources/Application/AppDelegate.swift b/SseuDamApp/Sources/Application/AppDelegate.swift index 367278a..adef294 100644 --- a/SseuDamApp/Sources/Application/AppDelegate.swift +++ b/SseuDamApp/Sources/Application/AppDelegate.swift @@ -9,8 +9,8 @@ import UIKit import UserNotifications import LogMacro import Data -import Firebase -import FirebaseAnalytics +//import Firebase +//import FirebaseAnalytics @@ -27,8 +27,8 @@ final class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCent setenv("FIRDebugEnabled", "1", 1) #endif - FirebaseApp.configure() - Analytics.setAnalyticsCollectionEnabled(true) +// FirebaseApp.configure() +// Analytics.setAnalyticsCollectionEnabled(true) let center = UNUserNotificationCenter.current() center.delegate = self diff --git a/SseuDamApp/Sources/Application/LiveDependencies.swift b/SseuDamApp/Sources/Application/LiveDependencies.swift index 7dc1305..85f8103 100644 --- a/SseuDamApp/Sources/Application/LiveDependencies.swift +++ b/SseuDamApp/Sources/Application/LiveDependencies.swift @@ -41,7 +41,7 @@ public enum LiveDependencies { dependencies.versionUseCase = VersionUseCase(repository: versionRepository) // Analytics - dependencies.analyticsUseCase = AnalyticsUseCase(repository: FirebaseAnalyticsRepository()) +// dependencies.analyticsUseCase = AnalyticsUseCase(repository: FirebaseAnalyticsRepository()) // Travel dependencies.fetchTravelsUseCase = FetchTravelsUseCase(repository: travelRepository) diff --git a/SseuDamApp/Sources/Reducer/AppFeature.swift b/SseuDamApp/Sources/Reducer/AppFeature.swift index f9d073c..8a7d01c 100644 --- a/SseuDamApp/Sources/Reducer/AppFeature.swift +++ b/SseuDamApp/Sources/Reducer/AppFeature.swift @@ -17,7 +17,7 @@ import Data @Reducer struct AppFeature { - @Dependency(\.analyticsUseCase) var analyticsUseCase +// @Dependency(\.analyticsUseCase) var analyticsUseCase // MARK: - State @ObservableState @@ -280,7 +280,7 @@ extension AppFeature { let deeplinkType = notification.userInfo?["deeplink_type"] as? String { // Analytics 이벤트 전송 - analyticsUseCase.track(.deeplink(DeeplinkEventData(deeplink: urlString, type: deeplinkType))) +// analyticsUseCase.track(.deeplink(DeeplinkEventData(deeplink: urlString, type: deeplinkType))) await send(.view(.handlePushNotificationDeepLink(urlString))) } diff --git a/Tuist/Package.swift b/Tuist/Package.swift index b2678dc..1cf27f8 100644 --- a/Tuist/Package.swift +++ b/Tuist/Package.swift @@ -10,6 +10,7 @@ let packageSettings = PackageSettings( "TCACoordinators": .framework, "Moya": .framework, "LogMacro": .framework, + "AppAuth": .framework // "FirebaseCore": .staticLibrary, // "FirebaseAuth": .staticLibrary, // "FirebaseFirestore": .staticLibrary, @@ -30,6 +31,6 @@ let package = Package( .package(url: "https://github.com/google/GoogleSignIn-iOS.git", from: "9.0.0"), .package(url: "https://github.com/Roy-wonji/LogMacro.git", from: "1.1.1"), .package(url: "https://github.com/Moya/Moya.git", exact: "15.0.3"), - .package(url: "https://github.com/firebase/firebase-ios-sdk.git", exact: "12.7.0") +// .package(url: "https://github.com/firebase/firebase-ios-sdk.git", exact: "12.7.0") ] )