diff --git a/Data/Project.swift b/Data/Project.swift index 0a82a83..0cf8652 100644 --- a/Data/Project.swift +++ b/Data/Project.swift @@ -8,13 +8,12 @@ let project = Project.makeFramework( .NetworkService, .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 c73d585..9e15aa0 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) -// } -//} +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/Data/Sources/DataSource/Remote/AuthRemoteDataSource.swift b/Data/Sources/DataSource/Remote/AuthRemoteDataSource.swift index 073063a..c538612 100644 --- a/Data/Sources/DataSource/Remote/AuthRemoteDataSource.swift +++ b/Data/Sources/DataSource/Remote/AuthRemoteDataSource.swift @@ -24,13 +24,16 @@ public protocol AuthRemoteDataSourceProtocol: Sendable { public struct AuthRemoteDataSource: AuthRemoteDataSourceProtocol { private let authProvider: MoyaProvider private let oauthProvider: MoyaProvider + private let noAuthProvider: MoyaProvider public init( authProvider: MoyaProvider = .authorized, - oauthProvider: MoyaProvider = .default + oauthProvider: MoyaProvider = .default, + noAuthProvider: MoyaProvider = .default ) { self.authProvider = authProvider self.oauthProvider = oauthProvider + self.noAuthProvider = noAuthProvider } public func refresh(token: String) async throws -> TokenResult { @@ -56,7 +59,7 @@ public struct AuthRemoteDataSource: AuthRemoteDataSourceProtocol { public func registerDeviceToken(token: String) async throws -> DeviceToken { let pendingKey = getOrCreatePendingKey() let body = DeviceTokenRequestDTO(deviceToken: token, pendingKey: pendingKey) - let response: BaseResponse = try await authProvider.request(.registerDeviceToken(body: body)) + let response: BaseResponse = try await noAuthProvider.request(.registerDeviceToken(body: body)) guard let data = response.data else { throw NetworkError.noData } if let returnedKey = data.pendingKey { persistPendingKey(returnedKey) } return data.toDomain() diff --git a/Data/Sources/Repository/Auth/AuthRepository.swift b/Data/Sources/Repository/Auth/AuthRepository.swift index d91eae6..145f233 100644 --- a/Data/Sources/Repository/Auth/AuthRepository.swift +++ b/Data/Sources/Repository/Auth/AuthRepository.swift @@ -16,7 +16,8 @@ final public class AuthRepository: AuthRepositoryProtocol { public init( remote: any AuthRemoteDataSourceProtocol = AuthRemoteDataSource( authProvider: MoyaProvider.authorized, - oauthProvider: MoyaProvider.default + oauthProvider: MoyaProvider.default, + noAuthProvider: MoyaProvider.default ) ) { self.remote = remote diff --git a/Domain/Sources/Repository/Analytics/AnalyticsRepositoryProtocol.swift b/Domain/Sources/Repository/Analytics/AnalyticsRepositoryProtocol.swift index d714cdc..880db1b 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 -//} +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 acb40f4..1d8a988 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 0e5fe8f..abf9bee 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 1a3528b..249aac9 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/SseuDamApp/Sources/Application/AppDelegate.swift b/SseuDamApp/Sources/Application/AppDelegate.swift index adef294..367278a 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/Tuist/Package.swift b/Tuist/Package.swift index 1cf27f8..e1727f5 100644 --- a/Tuist/Package.swift +++ b/Tuist/Package.swift @@ -10,7 +10,15 @@ let packageSettings = PackageSettings( "TCACoordinators": .framework, "Moya": .framework, "LogMacro": .framework, - "AppAuth": .framework + "AppAuth": .framework, + "AppAuthCore": .framework, + "GTMAppAuth": .framework, + "GTMSessionFetcherCore": .framework, + "IssueReporting": .framework, + "IssueReportingPackageSupport": .framework, + "XCTestDynamicOverlay": .framework, + "Clocks": .framework, + "ConcurrencyExtras": .framework // "FirebaseCore": .staticLibrary, // "FirebaseAuth": .staticLibrary, // "FirebaseFirestore": .staticLibrary, @@ -31,6 +39,7 @@ 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"), + .package(url: "https://github.com/openid/AppAuth-iOS.git", from: "2.0.0") ] ) diff --git a/Tuist/Plugins/SseuDamPlugin/ProjectDescriptionHelpers/ Project+Settings.swift b/Tuist/Plugins/SseuDamPlugin/ProjectDescriptionHelpers/ Project+Settings.swift index 973220c..b114c37 100644 --- a/Tuist/Plugins/SseuDamPlugin/ProjectDescriptionHelpers/ Project+Settings.swift +++ b/Tuist/Plugins/SseuDamPlugin/ProjectDescriptionHelpers/ Project+Settings.swift @@ -37,7 +37,8 @@ extension Settings { .setVersioningSystem() .setProvisioningProfileSpecifier("match Development \(Environment.organizationName)") .setDevelopmentTeam(Environment.organizationTeamId) - .setCFBundleDevelopmentRegion(), + .setCFBundleDevelopmentRegion() + .otherLinkerFlags(["-ObjC"]), configurations: [ .debug( name: .debug, diff --git a/fastlane/metadata/ko/release_notes.txt b/fastlane/metadata/ko/release_notes.txt index 18659ed..95a08d7 100644 --- a/fastlane/metadata/ko/release_notes.txt +++ b/fastlane/metadata/ko/release_notes.txt @@ -1,4 +1,6 @@ -[v 1.0.1] +[v 1.0.2] - 버그 수정 -- 지출내역 개선 +- 정산 내역 추가 +- 여행 설정 멤버 관리 페이지 추가 +- 여행 정산 페이지 추가