From 08a2cd3301b5f2eb73006be74f68cb4776c41c63 Mon Sep 17 00:00:00 2001 From: opficdev Date: Thu, 28 May 2026 10:46:23 +0900 Subject: [PATCH 1/9] =?UTF-8?q?feat:=20FirebaseAnalyticsCore=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DevLogInfra/DevLogInfra.xcodeproj/project.pbxproj | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Application/DevLogInfra/DevLogInfra.xcodeproj/project.pbxproj b/Application/DevLogInfra/DevLogInfra.xcodeproj/project.pbxproj index 957da7a4..1bd609b2 100644 --- a/Application/DevLogInfra/DevLogInfra.xcodeproj/project.pbxproj +++ b/Application/DevLogInfra/DevLogInfra.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 3CE81EA6CD7444739AC73633 /* DevLogInfra.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 592C8B7B099933759AB316A5 /* DevLogInfra.framework */; }; 5F7A14F2C5294547884212E1 /* FirebaseCore in Frameworks */ = {isa = PBXBuildFile; productRef = 06ADD17826F44543B09286DA /* FirebaseCore */; }; + 62DB1494C0A5B13B49950914 /* FirebaseAnalyticsCore in Frameworks */ = {isa = PBXBuildFile; productRef = 612F6D27B38A4D058F85C8AC /* FirebaseAnalyticsCore */; }; 65623F19F34A9A75BC72EE36 /* FirebaseFirestore in Frameworks */ = {isa = PBXBuildFile; productRef = CE8F32B22E13743B3FF3B66B /* FirebaseFirestore */; }; B11111111111111111111111 /* DevLogCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B55555555555555555555555 /* DevLogCore.framework */; }; E747320CAD03A579976E87F8 /* FirebaseFunctions in Frameworks */ = {isa = PBXBuildFile; productRef = 172AF9DE3BC54E9E0B4EFD19 /* FirebaseFunctions */; }; @@ -76,6 +77,7 @@ files = ( F75AA9259C6636CF733C4D82 /* Foundation.framework in Frameworks */, 5F7A14F2C5294547884212E1 /* FirebaseCore in Frameworks */, + 62DB1494C0A5B13B49950914 /* FirebaseAnalyticsCore in Frameworks */, FB5186BC5A89B7DADAB8A82A /* FirebaseAuth in Frameworks */, 65623F19F34A9A75BC72EE36 /* FirebaseFirestore in Frameworks */, E747320CAD03A579976E87F8 /* FirebaseFunctions in Frameworks */, @@ -181,6 +183,7 @@ ); name = DevLogInfra; packageProductDependencies = ( + 612F6D27B38A4D058F85C8AC /* FirebaseAnalyticsCore */, 06ADD17826F44543B09286DA /* FirebaseCore */, 2AC59F98E5A6BFA339C3E5BD /* FirebaseAuth */, CE8F32B22E13743B3FF3B66B /* FirebaseFirestore */, @@ -564,6 +567,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 612F6D27B38A4D058F85C8AC /* FirebaseAnalyticsCore */ = { + isa = XCSwiftPackageProductDependency; + package = 6A88F5113FA6A29A059E7035 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + productName = FirebaseAnalyticsCore; + }; 06ADD17826F44543B09286DA /* FirebaseCore */ = { isa = XCSwiftPackageProductDependency; package = 6A88F5113FA6A29A059E7035 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; From e0819d848a7ddbc302e2997191bf322890f019b8 Mon Sep 17 00:00:00 2001 From: opficdev Date: Thu, 28 May 2026 10:46:31 +0900 Subject: [PATCH 2/9] =?UTF-8?q?feat:=20Analytics=20=EA=B3=84=EC=B8=B5=20?= =?UTF-8?q?=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DevLogData/Sources/DataAssembler.swift | 6 +++ .../Sources/Protocol/AnalyticsService.swift | 14 ++++++ .../Repository/AnalyticsRepositoryImpl.swift | 31 +++++++++++++ .../Sources/DomainAssembler.swift | 7 +++ .../Sources/Entity/AnalyticsEvent.swift | 14 ++++++ .../Protocol/AnalyticsRepository.swift | 10 +++++ .../TrackAnalyticsEventUseCase.swift | 10 +++++ .../TrackAnalyticsEventUseCaseImpl.swift | 18 ++++++++ .../DevLogInfra/Sources/InfraAssembler.swift | 4 ++ .../FirebaseAnalyticsServiceImpl.swift | 43 +++++++++++++++++++ 10 files changed, 157 insertions(+) create mode 100644 Application/DevLogData/Sources/Protocol/AnalyticsService.swift create mode 100644 Application/DevLogData/Sources/Repository/AnalyticsRepositoryImpl.swift create mode 100644 Application/DevLogDomain/Sources/Entity/AnalyticsEvent.swift create mode 100644 Application/DevLogDomain/Sources/Protocol/AnalyticsRepository.swift create mode 100644 Application/DevLogDomain/Sources/UseCase/Analytics/TrackAnalyticsEventUseCase.swift create mode 100644 Application/DevLogDomain/Sources/UseCase/Analytics/TrackAnalyticsEventUseCaseImpl.swift create mode 100644 Application/DevLogInfra/Sources/Service/FirebaseAnalyticsServiceImpl.swift diff --git a/Application/DevLogData/Sources/DataAssembler.swift b/Application/DevLogData/Sources/DataAssembler.swift index f79bdee0..902ee0cb 100644 --- a/Application/DevLogData/Sources/DataAssembler.swift +++ b/Application/DevLogData/Sources/DataAssembler.swift @@ -79,6 +79,12 @@ public final class DataAssembler: Assembler { UserDataRepositoryImpl(userService: container.resolve(UserService.self)) } + container.register(AnalyticsRepository.self) { + AnalyticsRepositoryImpl( + analyticsService: container.resolve(AnalyticsService.self) + ) + } + container.register(PushNotificationRepository.self) { PushNotificationRepositoryImpl( pushNotificationService: container.resolve(PushNotificationService.self), diff --git a/Application/DevLogData/Sources/Protocol/AnalyticsService.swift b/Application/DevLogData/Sources/Protocol/AnalyticsService.swift new file mode 100644 index 00000000..710d9244 --- /dev/null +++ b/Application/DevLogData/Sources/Protocol/AnalyticsService.swift @@ -0,0 +1,14 @@ +// +// AnalyticsService.swift +// DevLogData +// +// Created by opfic on 5/27/26. +// + +public protocol AnalyticsService { + func trackScreenView(_ name: String) + func trackTodoCreate() + func trackTodoComplete() + func trackWebPageCreate() + func trackPushOpen() +} diff --git a/Application/DevLogData/Sources/Repository/AnalyticsRepositoryImpl.swift b/Application/DevLogData/Sources/Repository/AnalyticsRepositoryImpl.swift new file mode 100644 index 00000000..af36972a --- /dev/null +++ b/Application/DevLogData/Sources/Repository/AnalyticsRepositoryImpl.swift @@ -0,0 +1,31 @@ +// +// AnalyticsRepositoryImpl.swift +// DevLogData +// +// Created by opfic on 5/27/26. +// + +import DevLogDomain + +final class AnalyticsRepositoryImpl: AnalyticsRepository { + private let analyticsService: AnalyticsService + + init(analyticsService: AnalyticsService) { + self.analyticsService = analyticsService + } + + func track(_ event: AnalyticsEvent) { + switch event { + case .screenView(let name): + analyticsService.trackScreenView(name) + case .todoCreate: + analyticsService.trackTodoCreate() + case .todoComplete: + analyticsService.trackTodoComplete() + case .webPageCreate: + analyticsService.trackWebPageCreate() + case .pushOpen: + analyticsService.trackPushOpen() + } + } +} diff --git a/Application/DevLogDomain/Sources/DomainAssembler.swift b/Application/DevLogDomain/Sources/DomainAssembler.swift index 07b8fb26..4a78965c 100644 --- a/Application/DevLogDomain/Sources/DomainAssembler.swift +++ b/Application/DevLogDomain/Sources/DomainAssembler.swift @@ -11,6 +11,7 @@ public final class DomainAssembler: Assembler { public init() { } public func assemble(_ container: any DIContainer) { + registerAnalyticsUseCases(container) registerAuthUseCases(container) registerConnectivityUseCases(container) registerAuthProviderUseCases(container) @@ -24,6 +25,12 @@ public final class DomainAssembler: Assembler { } private extension DomainAssembler { + func registerAnalyticsUseCases(_ container: any DIContainer) { + container.register(TrackAnalyticsEventUseCase.self) { + TrackAnalyticsEventUseCaseImpl(container.resolve(AnalyticsRepository.self)) + } + } + func registerAuthUseCases(_ container: any DIContainer) { container.register(SignInUseCase.self) { SignInUseCaseImpl(container.resolve(AuthenticationRepository.self)) diff --git a/Application/DevLogDomain/Sources/Entity/AnalyticsEvent.swift b/Application/DevLogDomain/Sources/Entity/AnalyticsEvent.swift new file mode 100644 index 00000000..08da0aad --- /dev/null +++ b/Application/DevLogDomain/Sources/Entity/AnalyticsEvent.swift @@ -0,0 +1,14 @@ +// +// AnalyticsEvent.swift +// DevLogDomain +// +// Created by opfic on 5/28/26. +// + +public enum AnalyticsEvent { + case screenView(String) + case todoCreate + case todoComplete + case webPageCreate + case pushOpen +} diff --git a/Application/DevLogDomain/Sources/Protocol/AnalyticsRepository.swift b/Application/DevLogDomain/Sources/Protocol/AnalyticsRepository.swift new file mode 100644 index 00000000..f3887506 --- /dev/null +++ b/Application/DevLogDomain/Sources/Protocol/AnalyticsRepository.swift @@ -0,0 +1,10 @@ +// +// AnalyticsRepository.swift +// DevLogDomain +// +// Created by opfic on 5/27/26. +// + +public protocol AnalyticsRepository { + func track(_ event: AnalyticsEvent) +} diff --git a/Application/DevLogDomain/Sources/UseCase/Analytics/TrackAnalyticsEventUseCase.swift b/Application/DevLogDomain/Sources/UseCase/Analytics/TrackAnalyticsEventUseCase.swift new file mode 100644 index 00000000..8fb2ca52 --- /dev/null +++ b/Application/DevLogDomain/Sources/UseCase/Analytics/TrackAnalyticsEventUseCase.swift @@ -0,0 +1,10 @@ +// +// TrackAnalyticsEventUseCase.swift +// DevLogDomain +// +// Created by opfic on 5/27/26. +// + +public protocol TrackAnalyticsEventUseCase { + func execute(_ event: AnalyticsEvent) +} diff --git a/Application/DevLogDomain/Sources/UseCase/Analytics/TrackAnalyticsEventUseCaseImpl.swift b/Application/DevLogDomain/Sources/UseCase/Analytics/TrackAnalyticsEventUseCaseImpl.swift new file mode 100644 index 00000000..2a7d900a --- /dev/null +++ b/Application/DevLogDomain/Sources/UseCase/Analytics/TrackAnalyticsEventUseCaseImpl.swift @@ -0,0 +1,18 @@ +// +// TrackAnalyticsEventUseCaseImpl.swift +// DevLogDomain +// +// Created by opfic on 5/27/26. +// + +public final class TrackAnalyticsEventUseCaseImpl: TrackAnalyticsEventUseCase { + private let repository: AnalyticsRepository + + init(_ repository: AnalyticsRepository) { + self.repository = repository + } + + public func execute(_ event: AnalyticsEvent) { + repository.track(event) + } +} diff --git a/Application/DevLogInfra/Sources/InfraAssembler.swift b/Application/DevLogInfra/Sources/InfraAssembler.swift index b8d0ec04..bfa50b9d 100644 --- a/Application/DevLogInfra/Sources/InfraAssembler.swift +++ b/Application/DevLogInfra/Sources/InfraAssembler.swift @@ -16,6 +16,10 @@ public final class InfraAssembler: Assembler { FirebaseAppServiceImpl() } + container.register(AnalyticsService.self) { + FirebaseAnalyticsServiceImpl() + } + container.register(PushMessagingService.self) { PushMessagingServiceImpl() } diff --git a/Application/DevLogInfra/Sources/Service/FirebaseAnalyticsServiceImpl.swift b/Application/DevLogInfra/Sources/Service/FirebaseAnalyticsServiceImpl.swift new file mode 100644 index 00000000..6f8eff72 --- /dev/null +++ b/Application/DevLogInfra/Sources/Service/FirebaseAnalyticsServiceImpl.swift @@ -0,0 +1,43 @@ +// +// FirebaseAnalyticsServiceImpl.swift +// DevLogInfra +// +// Created by opfic on 5/27/26. +// + +import DevLogData +import FirebaseAnalytics + +final class FirebaseAnalyticsServiceImpl: AnalyticsService { + private enum EventName { + static let todoCreate = "todo_create" + static let todoComplete = "todo_complete" + static let webPageCreate = "webpage_create" + static let pushOpen = "push_open" + } + + func trackScreenView(_ name: String) { + Analytics.logEvent( + AnalyticsEventScreenView, + parameters: [ + AnalyticsParameterScreenName: name, + ] + ) + } + + func trackTodoCreate() { + Analytics.logEvent(EventName.todoCreate, parameters: nil) + } + + func trackTodoComplete() { + Analytics.logEvent(EventName.todoComplete, parameters: nil) + } + + func trackWebPageCreate() { + Analytics.logEvent(EventName.webPageCreate, parameters: nil) + } + + func trackPushOpen() { + Analytics.logEvent(EventName.pushOpen, parameters: nil) + } +} From ba67ab6721f79d3e5923099ff40891dda96503bc Mon Sep 17 00:00:00 2001 From: opficdev Date: Thu, 28 May 2026 10:46:37 +0900 Subject: [PATCH 3/9] =?UTF-8?q?feat:=20=ED=99=94=EB=A9=B4=20=EB=B6=84?= =?UTF-8?q?=EC=84=9D=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DevLogApp/Sources/App/DevLogApp.swift | 1 + .../Sources/Main/MainView.swift | 2 ++ .../Sources/Main/MainViewCoordinator.swift | 2 ++ .../Sources/Main/MainViewModel.swift | 31 +++++++++++++++++++ .../Sources/Root/RootView.swift | 4 ++- .../Sources/Root/RootViewModel.swift | 11 ++++++- 6 files changed, 49 insertions(+), 2 deletions(-) diff --git a/Application/DevLogApp/Sources/App/DevLogApp.swift b/Application/DevLogApp/Sources/App/DevLogApp.swift index 645d183a..3737e02a 100644 --- a/Application/DevLogApp/Sources/App/DevLogApp.swift +++ b/Application/DevLogApp/Sources/App/DevLogApp.swift @@ -27,6 +27,7 @@ struct DevLogApp: App { sessionUseCase: container.resolve(ObserveAuthSessionUseCase.self), networkConnectivityUseCase: container.resolve(ObserveNetworkConnectivityUseCase.self), systemThemeUseCase: container.resolve(ObserveSystemThemeUseCase.self), + trackAnalyticsEventUseCase: container.resolve(TrackAnalyticsEventUseCase.self), widgetURLTab: { MainTab(widgetURL: $0) }, pushNotificationTodoIdPublisher: PushNotificationRoute.shared.observe(), clearPushNotificationRoute: { PushNotificationRoute.shared.clear() } diff --git a/Application/DevLogPresentation/Sources/Main/MainView.swift b/Application/DevLogPresentation/Sources/Main/MainView.swift index 604b063f..59b2442b 100644 --- a/Application/DevLogPresentation/Sources/Main/MainView.swift +++ b/Application/DevLogPresentation/Sources/Main/MainView.swift @@ -42,6 +42,8 @@ struct MainView: View { coordinator.mainViewModel.send(.onAppear) } .onChange(of: selectedTab, initial: true) { _, newValue in + guard let newValue else { return } + coordinator.mainViewModel.send(.selectedTabChanged(newValue)) if newValue == .home { homeViewCoordinator.fetchData() } else if newValue == .today { diff --git a/Application/DevLogPresentation/Sources/Main/MainViewCoordinator.swift b/Application/DevLogPresentation/Sources/Main/MainViewCoordinator.swift index ce0565d9..493b069e 100644 --- a/Application/DevLogPresentation/Sources/Main/MainViewCoordinator.swift +++ b/Application/DevLogPresentation/Sources/Main/MainViewCoordinator.swift @@ -24,6 +24,7 @@ final class MainViewCoordinator { init(container: DIContainer) { self.diContainer = container self.mainViewModel = MainViewModel( + trackAnalyticsEventUseCase: container.resolve(TrackAnalyticsEventUseCase.self), unreadPushCountUseCase: container.resolve(ObserveUnreadPushCountUseCase.self) ) self.pushNotificationListViewModel = PushNotificationListViewModel( @@ -48,6 +49,7 @@ final class MainViewCoordinator { upsertTodoUseCase: diContainer.resolve(UpsertTodoUseCase.self), deleteTodoUseCase: diContainer.resolve(DeleteTodoUseCase.self), undoDeleteTodoUseCase: diContainer.resolve(UndoDeleteTodoUseCase.self), + trackAnalyticsEventUseCase: diContainer.resolve(TrackAnalyticsEventUseCase.self), category: category ) self.todoListViewModel = todoListViewModel diff --git a/Application/DevLogPresentation/Sources/Main/MainViewModel.swift b/Application/DevLogPresentation/Sources/Main/MainViewModel.swift index 707f948f..4679faaf 100644 --- a/Application/DevLogPresentation/Sources/Main/MainViewModel.swift +++ b/Application/DevLogPresentation/Sources/Main/MainViewModel.swift @@ -22,12 +22,14 @@ final class MainViewModel: Store { enum Action { case onAppear + case selectedTabChanged(MainTab) case setUnreadPushCount(Int) case setAlert(Bool) } enum SideEffect { case observeUnreadPushCount + case trackScreenView(MainTab) case updateBadgeCount(Int) } @@ -35,11 +37,14 @@ final class MainViewModel: Store { private let logger = Logger(category: "MainViewModel") private var cancellables = Set() private var isObservingUnreadPushCount = false + private let trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase private let unreadPushCountUseCase: ObserveUnreadPushCountUseCase init( + trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase, unreadPushCountUseCase: ObserveUnreadPushCountUseCase ) { + self.trackAnalyticsEventUseCase = trackAnalyticsEventUseCase self.unreadPushCountUseCase = unreadPushCountUseCase } @@ -53,6 +58,10 @@ final class MainViewModel: Store { isObservingUnreadPushCount = true sideEffects = [.observeUnreadPushCount] } + case .selectedTabChanged(let tab): + if tab.analyticsScreenName != nil { + sideEffects = [.trackScreenView(tab)] + } case .setUnreadPushCount(let count): state.unreadPushCount = count sideEffects = [.updateBadgeCount(count)] @@ -68,6 +77,8 @@ final class MainViewModel: Store { switch effect { case .observeUnreadPushCount: observeUnreadPushCount() + case .trackScreenView(let tab): + trackScreenView(tab) case .updateBadgeCount(let count): updateBadgeCount(count) } @@ -116,4 +127,24 @@ private extension MainViewModel { } } } + + func trackScreenView(_ tab: MainTab) { + guard let screenName = tab.analyticsScreenName else { return } + trackAnalyticsEventUseCase.execute(.screenView(screenName)) + } +} + +private extension MainTab { + var analyticsScreenName: String? { + switch self { + case .home: + return "home" + case .today: + return "today" + case .notification: + return nil + case .profile: + return "profile" + } + } } diff --git a/Application/DevLogPresentation/Sources/Root/RootView.swift b/Application/DevLogPresentation/Sources/Root/RootView.swift index 81c34307..0ba4bec5 100644 --- a/Application/DevLogPresentation/Sources/Root/RootView.swift +++ b/Application/DevLogPresentation/Sources/Root/RootView.swift @@ -23,6 +23,7 @@ public struct RootView: View { sessionUseCase: ObserveAuthSessionUseCase, networkConnectivityUseCase: ObserveNetworkConnectivityUseCase, systemThemeUseCase: ObserveSystemThemeUseCase, + trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase, widgetURLTab: @escaping (URL) -> MainTab?, pushNotificationTodoIdPublisher: AnyPublisher, clearPushNotificationRoute: @escaping () -> Void @@ -30,7 +31,8 @@ public struct RootView: View { self._viewModel = State(initialValue: RootViewModel( sessionUseCase: sessionUseCase, networkConnectivityUseCase: networkConnectivityUseCase, - systemThemeUseCase: systemThemeUseCase + systemThemeUseCase: systemThemeUseCase, + trackAnalyticsEventUseCase: trackAnalyticsEventUseCase )) self.widgetURLTab = widgetURLTab self.pushNotificationTodoIdPublisher = pushNotificationTodoIdPublisher diff --git a/Application/DevLogPresentation/Sources/Root/RootViewModel.swift b/Application/DevLogPresentation/Sources/Root/RootViewModel.swift index 73c336f5..3c63f0da 100644 --- a/Application/DevLogPresentation/Sources/Root/RootViewModel.swift +++ b/Application/DevLogPresentation/Sources/Root/RootViewModel.swift @@ -32,6 +32,7 @@ final class RootViewModel: Store { enum SideEffect { case clearApplicationBadgeCount + case trackLoginScreen } private(set) var state: State @@ -39,15 +40,18 @@ final class RootViewModel: Store { private let sessionUseCase: ObserveAuthSessionUseCase private let networkConnectivityUseCase: ObserveNetworkConnectivityUseCase private let systemThemeUseCase: ObserveSystemThemeUseCase + private let trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase init( sessionUseCase: ObserveAuthSessionUseCase, networkConnectivityUseCase: ObserveNetworkConnectivityUseCase, - systemThemeUseCase: ObserveSystemThemeUseCase + systemThemeUseCase: ObserveSystemThemeUseCase, + trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase ) { self.sessionUseCase = sessionUseCase self.networkConnectivityUseCase = networkConnectivityUseCase self.systemThemeUseCase = systemThemeUseCase + self.trackAnalyticsEventUseCase = trackAnalyticsEventUseCase self.state = State() setupNetworkObserving() @@ -74,6 +78,9 @@ final class RootViewModel: Store { state.theme = theme case .didLogined(let result): state.signIn = result + if !result { + effects = [.trackLoginScreen] + } } if self.state != state { self.state = state } @@ -84,6 +91,8 @@ final class RootViewModel: Store { switch effect { case .clearApplicationBadgeCount: UNUserNotificationCenter.current().setBadgeCount(0) { _ in } + case .trackLoginScreen: + trackAnalyticsEventUseCase.execute(.screenView("login")) } } } From 29cf7c7d907afd39ef408366115aa742e6b93d24 Mon Sep 17 00:00:00 2001 From: opficdev Date: Thu, 28 May 2026 10:46:42 +0900 Subject: [PATCH 4/9] =?UTF-8?q?feat:=20=EC=83=9D=EC=84=B1=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=20=EB=B0=8F=20Todo=20=EC=99=84=EB=A3=8C=20?= =?UTF-8?q?=EB=B6=84=EC=84=9D=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Home/Home/HomeViewCoordinator.swift | 3 ++- .../Sources/Home/Home/HomeViewModel.swift | 7 ++++++- .../Sources/Home/TodoListViewModel.swift | 7 +++++++ .../Sources/Today/TodayViewCoordinator.swift | 3 ++- .../DevLogPresentation/Sources/Today/TodayViewModel.swift | 6 +++++- 5 files changed, 22 insertions(+), 4 deletions(-) diff --git a/Application/DevLogPresentation/Sources/Home/Home/HomeViewCoordinator.swift b/Application/DevLogPresentation/Sources/Home/Home/HomeViewCoordinator.swift index 0eafabc7..cfb78512 100644 --- a/Application/DevLogPresentation/Sources/Home/Home/HomeViewCoordinator.swift +++ b/Application/DevLogPresentation/Sources/Home/Home/HomeViewCoordinator.swift @@ -41,7 +41,8 @@ final class HomeViewCoordinator { upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self), fetchTodosUseCase: fetchTodosUseCase, fetchWebPagesUseCase: fetchWebPagesUseCase, - networkConnectivityUseCase: container.resolve(ObserveNetworkConnectivityUseCase.self) + networkConnectivityUseCase: container.resolve(ObserveNetworkConnectivityUseCase.self), + trackAnalyticsEventUseCase: container.resolve(TrackAnalyticsEventUseCase.self) ) } diff --git a/Application/DevLogPresentation/Sources/Home/Home/HomeViewModel.swift b/Application/DevLogPresentation/Sources/Home/Home/HomeViewModel.swift index dbda9812..c492ae5d 100644 --- a/Application/DevLogPresentation/Sources/Home/Home/HomeViewModel.swift +++ b/Application/DevLogPresentation/Sources/Home/Home/HomeViewModel.swift @@ -110,6 +110,7 @@ final class HomeViewModel: Store { private let fetchTodosUseCase: FetchTodosUseCase private let fetchWebPagesUseCase: FetchWebPagesUseCase private let networkConnectivityUseCase: ObserveNetworkConnectivityUseCase + private let trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase private let loadingState = LoadingState() private var deletedWebPageURLString: String? private var cancellables = Set() @@ -123,7 +124,8 @@ final class HomeViewModel: Store { upsertTodoUseCase: UpsertTodoUseCase, fetchTodosUseCase: FetchTodosUseCase, fetchWebPagesUseCase: FetchWebPagesUseCase, - networkConnectivityUseCase: ObserveNetworkConnectivityUseCase + networkConnectivityUseCase: ObserveNetworkConnectivityUseCase, + trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase ) { self.fetchPreferencesUseCase = fetchPreferencesUseCase self.updatePreferencesUseCase = updatePreferencesUseCase @@ -134,6 +136,7 @@ final class HomeViewModel: Store { self.fetchTodosUseCase = fetchTodosUseCase self.fetchWebPagesUseCase = fetchWebPagesUseCase self.networkConnectivityUseCase = networkConnectivityUseCase + self.trackAnalyticsEventUseCase = trackAnalyticsEventUseCase setupNetworkObserving() } @@ -186,6 +189,7 @@ final class HomeViewModel: Store { do { defer { endLoading(for: .overlay, mode: .delayed) } try await upsertTodoUseCase.execute(todo) + trackAnalyticsEventUseCase.execute(.todoCreate) let page = try await fetchRecentTodos() let items = page.items .filter { $0.createdAt != $0.updatedAt } @@ -217,6 +221,7 @@ final class HomeViewModel: Store { do { defer { endLoading(for: .overlay, mode: .delayed) } try await addWebPageUseCase.execute(urlString) + trackAnalyticsEventUseCase.execute(.webPageCreate) let pages = try await fetchWebPagesUseCase.execute("") send(.updateWebPages(pages.map { WebPageItem(from: $0) })) } catch { diff --git a/Application/DevLogPresentation/Sources/Home/TodoListViewModel.swift b/Application/DevLogPresentation/Sources/Home/TodoListViewModel.swift index 37b9552f..ebab4b66 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoListViewModel.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoListViewModel.swift @@ -89,6 +89,7 @@ final class TodoListViewModel: Store { private let upsertTodoUseCase: UpsertTodoUseCase private let deleteTodoUseCase: DeleteTodoUseCase private let undoDeleteTodoUseCase: UndoDeleteTodoUseCase + private let trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase private let loadingState = LoadingState() private var undoTodoId: String? private var nextCursor: TodoCursor? @@ -101,6 +102,7 @@ final class TodoListViewModel: Store { upsertTodoUseCase: UpsertTodoUseCase, deleteTodoUseCase: DeleteTodoUseCase, undoDeleteTodoUseCase: UndoDeleteTodoUseCase, + trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase, category: TodoCategory ) { self.fetchTodosUseCase = fetchTodosUseCase @@ -108,6 +110,7 @@ final class TodoListViewModel: Store { self.upsertTodoUseCase = upsertTodoUseCase self.deleteTodoUseCase = deleteTodoUseCase self.undoDeleteTodoUseCase = undoDeleteTodoUseCase + self.trackAnalyticsEventUseCase = trackAnalyticsEventUseCase self.category = category self.state = State( query: TodoQuery(categoryId: category.storageValue) @@ -209,6 +212,7 @@ final class TodoListViewModel: Store { do { defer { endLoading(.delayed) } try await upsertTodoUseCase.execute(item) + trackAnalyticsEventUseCase.execute(.todoCreate) send(.refresh) } catch { send(.setAlert(true)) @@ -225,6 +229,9 @@ final class TodoListViewModel: Store { todo.completedAt = todo.isCompleted ? now : nil todo.updatedAt = now try await upsertTodoUseCase.execute(todo) + if todo.isCompleted { + trackAnalyticsEventUseCase.execute(.todoComplete) + } guard let todoListItem = TodoListItem(from: todo) else { send(.setAlert(true)) return diff --git a/Application/DevLogPresentation/Sources/Today/TodayViewCoordinator.swift b/Application/DevLogPresentation/Sources/Today/TodayViewCoordinator.swift index 290bb0d3..22b052bf 100644 --- a/Application/DevLogPresentation/Sources/Today/TodayViewCoordinator.swift +++ b/Application/DevLogPresentation/Sources/Today/TodayViewCoordinator.swift @@ -21,7 +21,8 @@ final class TodayViewCoordinator { fetchTodoByIdUseCase: container.resolve(FetchTodoByIdUseCase.self), upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self), fetchTodayDisplayOptionsUseCase: container.resolve(FetchTodayDisplayOptionsUseCase.self), - updateTodayDisplayOptionsUseCase: container.resolve(UpdateTodayDisplayOptionsUseCase.self) + updateTodayDisplayOptionsUseCase: container.resolve(UpdateTodayDisplayOptionsUseCase.self), + trackAnalyticsEventUseCase: container.resolve(TrackAnalyticsEventUseCase.self) ) } diff --git a/Application/DevLogPresentation/Sources/Today/TodayViewModel.swift b/Application/DevLogPresentation/Sources/Today/TodayViewModel.swift index 0281bfbb..ccaef572 100644 --- a/Application/DevLogPresentation/Sources/Today/TodayViewModel.swift +++ b/Application/DevLogPresentation/Sources/Today/TodayViewModel.swift @@ -83,6 +83,7 @@ final class TodayViewModel: Store { private let fetchTodoByIdUseCase: FetchTodoByIdUseCase private let upsertTodoUseCase: UpsertTodoUseCase private let updateTodayDisplayOptionsUseCase: UpdateTodayDisplayOptionsUseCase + private let trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase private let loadingState = LoadingState() init( @@ -90,12 +91,14 @@ final class TodayViewModel: Store { fetchTodoByIdUseCase: FetchTodoByIdUseCase, upsertTodoUseCase: UpsertTodoUseCase, fetchTodayDisplayOptionsUseCase: FetchTodayDisplayOptionsUseCase, - updateTodayDisplayOptionsUseCase: UpdateTodayDisplayOptionsUseCase + updateTodayDisplayOptionsUseCase: UpdateTodayDisplayOptionsUseCase, + trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase ) { self.fetchTodosUseCase = fetchTodosUseCase self.fetchTodoByIdUseCase = fetchTodoByIdUseCase self.upsertTodoUseCase = upsertTodoUseCase self.updateTodayDisplayOptionsUseCase = updateTodayDisplayOptionsUseCase + self.trackAnalyticsEventUseCase = trackAnalyticsEventUseCase self.state.displayOptions = fetchTodayDisplayOptionsUseCase.execute() } @@ -235,6 +238,7 @@ final class TodayViewModel: Store { todo.completedAt = now todo.updatedAt = now try await upsertTodoUseCase.execute(todo) + trackAnalyticsEventUseCase.execute(.todoComplete) send(.removeTodo(todo.id)) } catch { send(.setAlert(true)) From 55fc9891a5588855be213094746401c2842bc360 Mon Sep 17 00:00:00 2001 From: opficdev Date: Thu, 28 May 2026 10:46:49 +0900 Subject: [PATCH 5/9] =?UTF-8?q?feat:=20=ED=91=B8=EC=8B=9C=20=EC=98=A4?= =?UTF-8?q?=ED=94=88=20=EB=B6=84=EC=84=9D=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/Assembler/AppLayerAssembler.swift | 5 +++++ .../Sources/App/Delegate/AppDelegate.swift | 6 +++-- .../Handler/PushNotificationOpenHandler.swift | 22 +++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 Application/DevLogApp/Sources/App/Handler/PushNotificationOpenHandler.swift diff --git a/Application/DevLogApp/Sources/App/Assembler/AppLayerAssembler.swift b/Application/DevLogApp/Sources/App/Assembler/AppLayerAssembler.swift index 12fc5e13..9e2589db 100644 --- a/Application/DevLogApp/Sources/App/Assembler/AppLayerAssembler.swift +++ b/Application/DevLogApp/Sources/App/Assembler/AppLayerAssembler.swift @@ -31,5 +31,10 @@ final class AppLayerAssembler: Assembler { userService: container.resolve(UserService.self) ) } + container.register(PushNotificationOpenHandler.self) { + PushNotificationOpenHandler( + trackAnalyticsEventUseCase: container.resolve(TrackAnalyticsEventUseCase.self) + ) + } } } diff --git a/Application/DevLogApp/Sources/App/Delegate/AppDelegate.swift b/Application/DevLogApp/Sources/App/Delegate/AppDelegate.swift index c856f936..294bb6af 100644 --- a/Application/DevLogApp/Sources/App/Delegate/AppDelegate.swift +++ b/Application/DevLogApp/Sources/App/Delegate/AppDelegate.swift @@ -52,8 +52,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // 앱이 완전 종료되어도, 알림을 통해 앱이 시작된 경우 처리 if let remoteNotification = launchOptions?[.remoteNotification] as? [AnyHashable: Any] { + let handler = container.resolve(PushNotificationOpenHandler.self) Task { @MainActor in - PushNotificationRoute.shared.handlePushTap(userInfo: remoteNotification) + handler.handlePushOpen(userInfo: remoteNotification) } } @@ -110,8 +111,9 @@ extension AppDelegate: UNUserNotificationCenterDelegate { ) { logger.info("Tapped notification: \(response.notification.request.content.userInfo)") let userInfo = response.notification.request.content.userInfo + let handler = container.resolve(PushNotificationOpenHandler.self) Task { @MainActor in - PushNotificationRoute.shared.handlePushTap(userInfo: userInfo) + handler.handlePushOpen(userInfo: userInfo) } completionHandler() } diff --git a/Application/DevLogApp/Sources/App/Handler/PushNotificationOpenHandler.swift b/Application/DevLogApp/Sources/App/Handler/PushNotificationOpenHandler.swift new file mode 100644 index 00000000..c54d5790 --- /dev/null +++ b/Application/DevLogApp/Sources/App/Handler/PushNotificationOpenHandler.swift @@ -0,0 +1,22 @@ +// +// PushNotificationOpenHandler.swift +// DevLog +// +// Created by opfic on 5/28/26. +// + +import Foundation +import DevLogDomain + +final class PushNotificationOpenHandler { + private let trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase + + init(trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase) { + self.trackAnalyticsEventUseCase = trackAnalyticsEventUseCase + } + + func handlePushOpen(userInfo: [AnyHashable: Any]) { + trackAnalyticsEventUseCase.execute(.pushOpen) + PushNotificationRoute.shared.handlePushTap(userInfo: userInfo) + } +} From e207364fc48db2c56c4bbc2ba7e3c8e5c0e5fe7d Mon Sep 17 00:00:00 2001 From: opficdev Date: Thu, 28 May 2026 10:46:58 +0900 Subject: [PATCH 6/9] =?UTF-8?q?test:=20=EC=9B=B9=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=82=AD=EC=A0=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=B3=B4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Tests/WebPage/DeleteWebPageTests.swift | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Application/DevLogPresentation/Tests/WebPage/DeleteWebPageTests.swift b/Application/DevLogPresentation/Tests/WebPage/DeleteWebPageTests.swift index 78726cf5..2ee7ea76 100644 --- a/Application/DevLogPresentation/Tests/WebPage/DeleteWebPageTests.swift +++ b/Application/DevLogPresentation/Tests/WebPage/DeleteWebPageTests.swift @@ -32,6 +32,7 @@ struct DeleteWebPageTests { ] ) let observeNetworkConnectivityUseCaseSpy = ObserveNetworkConnectivityUseCaseSpy() + let trackAnalyticsEventUseCaseSpy = TrackAnalyticsEventUseCaseSpy() let homeViewModel = HomeViewModel( fetchPreferencesUseCase: fetchTodoCategoryPreferencesUseCaseSpy, @@ -42,7 +43,8 @@ struct DeleteWebPageTests { upsertTodoUseCase: upsertTodoUseCaseSpy, fetchTodosUseCase: fetchTodosUseCaseSpy, fetchWebPagesUseCase: fetchWebPagesUseCaseSpy, - networkConnectivityUseCase: observeNetworkConnectivityUseCaseSpy + networkConnectivityUseCase: observeNetworkConnectivityUseCaseSpy, + trackAnalyticsEventUseCase: trackAnalyticsEventUseCaseSpy ) homeViewModel.send(.fetchData) @@ -84,6 +86,7 @@ struct DeleteWebPageTests { ] ) let observeNetworkConnectivityUseCaseSpy = ObserveNetworkConnectivityUseCaseSpy() + let trackAnalyticsEventUseCaseSpy = TrackAnalyticsEventUseCaseSpy() let homeViewModel = HomeViewModel( fetchPreferencesUseCase: fetchTodoCategoryPreferencesUseCaseSpy, @@ -94,7 +97,8 @@ struct DeleteWebPageTests { upsertTodoUseCase: upsertTodoUseCaseSpy, fetchTodosUseCase: fetchTodosUseCaseSpy, fetchWebPagesUseCase: fetchWebPagesUseCaseSpy, - networkConnectivityUseCase: observeNetworkConnectivityUseCaseSpy + networkConnectivityUseCase: observeNetworkConnectivityUseCaseSpy, + trackAnalyticsEventUseCase: trackAnalyticsEventUseCaseSpy ) homeViewModel.send(.fetchData) @@ -119,3 +123,7 @@ struct DeleteWebPageTests { #expect(!restoredWebPageItem.isHidden) } } + +private struct TrackAnalyticsEventUseCaseSpy: TrackAnalyticsEventUseCase { + func execute(_ event: AnalyticsEvent) { } +} From d42914a4168b7d7e142d624d3db556390d0c2ec2 Mon Sep 17 00:00:00 2001 From: opficdev Date: Thu, 28 May 2026 11:04:35 +0900 Subject: [PATCH 7/9] =?UTF-8?q?chore:=20=EC=9E=90=EB=8F=99=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EC=B6=94=EC=A0=81=20=EB=B9=84=ED=99=9C=EC=84=B1?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Application/DevLogApp/Sources/Resource/Info.plist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Application/DevLogApp/Sources/Resource/Info.plist b/Application/DevLogApp/Sources/Resource/Info.plist index 13e3de1b..f3bd7703 100644 --- a/Application/DevLogApp/Sources/Resource/Info.plist +++ b/Application/DevLogApp/Sources/Resource/Info.plist @@ -20,6 +20,8 @@ FirebaseAppDelegateProxyEnabled + FirebaseAutomaticScreenReportingEnabled + GIDClientID $(CLIENT_ID) GITHUB_CLIENT_ID From df098a1c9a4a7fc3d80217aa8d71434b1d4ebfcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EC=9C=A4=EC=A7=84?= Date: Thu, 28 May 2026 11:24:50 +0900 Subject: [PATCH 8/9] =?UTF-8?q?refactor:=20handler=EC=97=90=20MainActor=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../Sources/App/Handler/PushNotificationOpenHandler.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Application/DevLogApp/Sources/App/Handler/PushNotificationOpenHandler.swift b/Application/DevLogApp/Sources/App/Handler/PushNotificationOpenHandler.swift index c54d5790..765e7c14 100644 --- a/Application/DevLogApp/Sources/App/Handler/PushNotificationOpenHandler.swift +++ b/Application/DevLogApp/Sources/App/Handler/PushNotificationOpenHandler.swift @@ -8,6 +8,7 @@ import Foundation import DevLogDomain +@MainActor final class PushNotificationOpenHandler { private let trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase From 3acf52c03fb9b3c479bcb49c1a121e6d07f3e69b Mon Sep 17 00:00:00 2001 From: opficdev Date: Thu, 28 May 2026 11:30:04 +0900 Subject: [PATCH 9/9] =?UTF-8?q?refactor:=20handler=EC=97=90=EC=84=9C=20Mai?= =?UTF-8?q?nActor=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/App/Handler/PushNotificationOpenHandler.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Application/DevLogApp/Sources/App/Handler/PushNotificationOpenHandler.swift b/Application/DevLogApp/Sources/App/Handler/PushNotificationOpenHandler.swift index 765e7c14..c54d5790 100644 --- a/Application/DevLogApp/Sources/App/Handler/PushNotificationOpenHandler.swift +++ b/Application/DevLogApp/Sources/App/Handler/PushNotificationOpenHandler.swift @@ -8,7 +8,6 @@ import Foundation import DevLogDomain -@MainActor final class PushNotificationOpenHandler { private let trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase