From 9e3da8b6dadc719134c8c55c672a5e3071b804a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=95?= Date: Tue, 28 Oct 2025 20:32:00 +0900 Subject: [PATCH 01/20] =?UTF-8?q?[fix]=20#208=20=EC=84=A4=EC=A0=95=20->=20?= =?UTF-8?q?=EB=8B=89=EB=84=A4=EC=9E=84=20=EB=8B=A4=ED=81=AC=EB=AA=A8?= =?UTF-8?q?=EB=93=9C=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FeatureSetting/Sources/Setting/PokitSettingView.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Projects/Feature/FeatureSetting/Sources/Setting/PokitSettingView.swift b/Projects/Feature/FeatureSetting/Sources/Setting/PokitSettingView.swift index ac0f33ec..9beed405 100644 --- a/Projects/Feature/FeatureSetting/Sources/Setting/PokitSettingView.swift +++ b/Projects/Feature/FeatureSetting/Sources/Setting/PokitSettingView.swift @@ -90,6 +90,8 @@ private extension PokitSettingView { .frame(width: 40, height: 40) Text(store.user?.nickname ?? "") .pokitFont(.b1(.m)) + .foregroundStyle(.pokit(.text(.primary))) + Spacer() PokitTextButton( "프로필 편집", From 06dce6dccdecac5fea19a97b22ee9fe571916607 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=95?= Date: Tue, 28 Oct 2025 20:35:23 +0900 Subject: [PATCH 02/20] =?UTF-8?q?[fix]=20#208=20=ED=8F=AC=ED=82=B7=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EB=82=B4=20=EC=9D=B4=EB=8F=99=20=EB=B0=94?= =?UTF-8?q?=ED=85=80=EC=8B=9C=ED=8A=B8=20=EB=86=92=EC=9D=B4=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FeatureCategoryDetail/Sources/CategoryDetailView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailView.swift b/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailView.swift index 7e319ed7..0e66d0ff 100644 --- a/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailView.swift +++ b/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailView.swift @@ -98,6 +98,7 @@ public extension CategoryDetailView { action: { send(.카테고리_선택했을때($0)) } ) .presentationDragIndicator(.visible) + .presentationDetents([.medium, .large]) } else { PokitLoading() .presentationDragIndicator(.visible) From cccfd97da61dc4c689715a3dd37b5bd407b867e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=95?= Date: Tue, 28 Oct 2025 20:35:52 +0900 Subject: [PATCH 03/20] =?UTF-8?q?[fix]=20#208=20pluscategory=20=EB=B0=94?= =?UTF-8?q?=ED=85=80=EC=8B=9C=ED=8A=B8=20ios=2026=20=EB=8C=80=EC=9D=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/Sources/MainTab/MainTabFeatureView.swift | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Projects/App/Sources/MainTab/MainTabFeatureView.swift b/Projects/App/Sources/MainTab/MainTabFeatureView.swift index 4dcee06a..6388ef08 100644 --- a/Projects/App/Sources/MainTab/MainTabFeatureView.swift +++ b/Projects/App/Sources/MainTab/MainTabFeatureView.swift @@ -269,7 +269,14 @@ private extension MainTabView { var body: some View { GeometryReader { proxy in - let bottomSafeArea = proxy.safeAreaInsets.bottom + let bottomPadding: CGFloat = { + if #available(iOS 26.0, *) { + return 32 + } else { + return 48 + } + }() + HStack(spacing: 20) { Spacer() @@ -302,7 +309,7 @@ private extension MainTabView { Spacer() } - .padding(.bottom, 48 - bottomSafeArea) + .padding(.bottom, bottomPadding) .padding(.top, 36) .pokitPresentationCornerRadius() .pokitPresentationBackground() @@ -315,6 +322,7 @@ private extension MainTabView { } .presentationDetents([.height(self.height)]) } + .ignoresSafeArea(edges: .bottom) } } } From dfc1f399ca0d351d30f237f1080b5a12e8d5d263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=95?= Date: Tue, 28 Oct 2025 20:36:09 +0900 Subject: [PATCH 04/20] =?UTF-8?q?[fix]=20#208=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=20=EA=B8=B0=EB=B3=B8=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EA=B0=80=EC=9E=A5=EC=9E=90=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/PokitProfileBottomSheet.swift | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Projects/DSKit/Sources/Components/PokitProfileBottomSheet.swift b/Projects/DSKit/Sources/Components/PokitProfileBottomSheet.swift index 8a3df705..55f76f00 100644 --- a/Projects/DSKit/Sources/Components/PokitProfileBottomSheet.swift +++ b/Projects/DSKit/Sources/Components/PokitProfileBottomSheet.swift @@ -44,17 +44,21 @@ public extension PokitProfileBottomSheet { transaction: .init(animation: .pokitDissolve) ) { phase in if let image = phase.image { + let isSelected = item.imageURL == selectedImage?.imageURL + Button(action: { delegateSend?(.이미지_선택했을때(item)) }) { image .resizable() .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) } .buttonStyle(.plain) - .overlay { - if let selectedImage, item.imageURL == selectedImage.imageURL { - RoundedRectangle(cornerRadius: 12, style: .continuous) - .stroke(.pokit(.border(.brand)), lineWidth: 2) - } + .overlay(if: isSelected) { + RoundedRectangle(cornerRadius: 12, style: .continuous) + .stroke(.pokit(.border(.brand)), lineWidth: 2) + } + .overlay(if: !isSelected && item.id == 33) { + RoundedRectangle(cornerRadius: 12, style: .continuous) + .stroke(.pokit(.border(.tertiary)), lineWidth: 2) } } else { PokitSpinner() From d543a11a870e73d610f7ada3171c4267462135ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=95?= Date: Tue, 28 Oct 2025 20:42:26 +0900 Subject: [PATCH 05/20] =?UTF-8?q?[feat]=20#208=20Amplitude=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 --- Tuist/Package.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tuist/Package.swift b/Tuist/Package.swift index d669a4e3..f6dc4df2 100644 --- a/Tuist/Package.swift +++ b/Tuist/Package.swift @@ -29,6 +29,7 @@ let package = Package( .package(url: "https://github.com/Kitura/Swift-JWT", from: "4.0.1"), .package(url: "https://github.com/kean/Nuke", from: "12.8.0"), .package(url: "https://github.com/scinfu/SwiftSoup", "2.7.0" ..< "2.7.5"), - .package(url: "https://github.com/kakao/kakao-ios-sdk", from: "2.22.5") + .package(url: "https://github.com/kakao/kakao-ios-sdk", from: "2.22.5"), + .package(url: "https://github.com/amplitude/Amplitude-Swift", from: "1.15.2") ] ) From 54bb9b5c5faefed24e185263d8be7a1b44457413 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=95?= Date: Tue, 25 Nov 2025 18:30:40 +0900 Subject: [PATCH 06/20] =?UTF-8?q?[feat]=20#208=20apmplitude=20=EA=B0=9D?= =?UTF-8?q?=EC=B2=B4=20=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/CoreKit/Project.swift | 1 + .../Core/Analytics/AmplitudeManager.swift | 73 ++++++++++ .../Core/Analytics/AnalyticsEvent.swift | 135 ++++++++++++++++++ .../DependencyValues+Analytics.swift | 51 +++++++ 4 files changed, 260 insertions(+) create mode 100644 Projects/CoreKit/Sources/Core/Analytics/AmplitudeManager.swift create mode 100644 Projects/CoreKit/Sources/Core/Analytics/AnalyticsEvent.swift create mode 100644 Projects/CoreKit/Sources/Core/Analytics/DependencyValues+Analytics.swift diff --git a/Projects/CoreKit/Project.swift b/Projects/CoreKit/Project.swift index 2679bc2b..a42f182a 100644 --- a/Projects/CoreKit/Project.swift +++ b/Projects/CoreKit/Project.swift @@ -36,6 +36,7 @@ let coreKit: Target = .target( .external(name: "KakaoSDKCommon"), .external(name: "KakaoSDKShare"), .external(name: "KakaoSDKTemplate"), + .external(name: "AmplitudeSwift"), ], settings: .settings() ) diff --git a/Projects/CoreKit/Sources/Core/Analytics/AmplitudeManager.swift b/Projects/CoreKit/Sources/Core/Analytics/AmplitudeManager.swift new file mode 100644 index 00000000..dfb58ae8 --- /dev/null +++ b/Projects/CoreKit/Sources/Core/Analytics/AmplitudeManager.swift @@ -0,0 +1,73 @@ +import Foundation +import AmplitudeSwift + +/// Amplitude Analytics 관리자 +public final class AmplitudeManager { + public static let shared = AmplitudeManager() + + private var amplitude: Amplitude? + + private init() {} + + /// Amplitude 초기화 + /// - Parameters: + /// - apiKey: Amplitude API Key + /// - userId: 사용자 ID (옵셔널) + public func initialize(apiKey: String, userId: String? = nil) { + amplitude = Amplitude(configuration: Configuration( + apiKey: apiKey + )) + + if let userId = userId { + setUserId(userId) + } + } + + /// 사용자 ID 설정 + /// - Parameter userId: 사용자 ID + public func setUserId(_ userId: String) { + amplitude?.setUserId(userId: userId) + } + + /// 사용자 속성 설정 + /// - Parameter properties: 사용자 속성 + public func setUserProperties(_ properties: [String: Any]) { + let identify = Identify() + properties.forEach { key, value in + identify.set(property: key, value: value) + } + amplitude?.identify(identify: identify) + } + + /// 이벤트 전송 + /// - Parameter event: AnalyticsEvent + public func track(_ event: AnalyticsEvent) { + guard let amplitude = amplitude else { + print("⚠️ Amplitude가 초기화되지 않았습니다.") + return + } + + let eventProperties = event.properties.isEmpty ? nil : event.properties + amplitude.track( + eventType: event.eventName, + eventProperties: eventProperties + ) + + #if DEBUG + print("📊 [Analytics] \(event.eventName)") + if let properties = eventProperties { + print(" Properties: \(properties)") + } + #endif + } + + /// 이벤트 버퍼 즉시 전송 + public func flush() { + amplitude?.flush() + } + + /// Amplitude 리셋 (로그아웃 시 사용) + public func reset() { + amplitude?.reset() + } +} diff --git a/Projects/CoreKit/Sources/Core/Analytics/AnalyticsEvent.swift b/Projects/CoreKit/Sources/Core/Analytics/AnalyticsEvent.swift new file mode 100644 index 00000000..b877f1a6 --- /dev/null +++ b/Projects/CoreKit/Sources/Core/Analytics/AnalyticsEvent.swift @@ -0,0 +1,135 @@ +import Foundation + +/// Analytics 이벤트 타입 +public enum AnalyticsEvent { + case app_open(deviceOS: String, appVersion: String) + case view_splash + case login_start(method: LoginMethod) + case login_complete(method: LoginMethod) + case interest_select(interests: [String]) + case onboarding_complete + case view_home_pokit(entryPoint: String) + case view_home_recommend(entryPoint: String) + case add_folder(folderName: String) + case add_link(folderId: String, linkDomain: String) + case view_folder_detail(folderId: String) + case view_link_detail(linkId: String, linkDomain: String) + case share_link(linkId: String, shareTarget: String) + case session_end(duration: Int) + + /// 이벤트명 (rawValue) + public var eventName: String { + switch self { + case .app_open: return "app_open" + case .view_splash: return "view_splash" + case .login_start: return "login_start" + case .login_complete: return "login_complete" + case .interest_select: return "interest_select" + case .onboarding_complete: return "onboarding_complete" + case .view_home_pokit: return "view_home_pokit" + case .view_home_recommend: return "view_home_recommend" + case .add_folder: return "add_folder" + case .add_link: return "add_link" + case .view_folder_detail: return "view_folder_detail" + case .view_link_detail: return "view_link_detail" + case .share_link: return "share_link" + case .session_end: return "session_end" + } + } + + /// Amplitude에 전송할 속성값 + public var properties: [String: Any] { + switch self { + case let .app_open(deviceOS, appVersion): + return [ + PropertyKey.device_os.rawValue: deviceOS, + PropertyKey.app_version.rawValue: appVersion + ] + + case .view_splash: + return [:] + + case let .login_start(method): + return [PropertyKey.method.rawValue: method.rawValue] + + case let .login_complete(method): + return [PropertyKey.method.rawValue: method.rawValue] + + case let .interest_select(interests): + return [PropertyKey.interests.rawValue: interests] + + case .onboarding_complete: + return [:] + + case let .view_home_pokit(entryPoint): + return [PropertyKey.entry_point.rawValue: entryPoint] + + case let .view_home_recommend(entryPoint): + return [PropertyKey.entry_point.rawValue: entryPoint] + + case let .add_folder(folderName): + return [PropertyKey.folder_name.rawValue: folderName] + + case let .add_link(folderId, linkDomain): + return [ + PropertyKey.folder_id.rawValue: folderId, + PropertyKey.link_domain.rawValue: linkDomain + ] + + case let .view_folder_detail(folderId): + return [PropertyKey.folder_id.rawValue: folderId] + + case let .view_link_detail(linkId, linkDomain): + return [ + PropertyKey.link_id.rawValue: linkId, + PropertyKey.link_domain.rawValue: linkDomain + ] + + case let .share_link(linkId, shareTarget): + return [ + PropertyKey.link_id.rawValue: linkId, + PropertyKey.share_target.rawValue: shareTarget + ] + + case let .session_end(duration): + return [PropertyKey.duration.rawValue: duration] + } + } +} + +/// Analytics 속성 키 +public enum PropertyKey: String { + case device_os + case app_version + case method + case interests + case entry_point + case folder_name + case folder_id + case link_domain + case link_id + case share_target + case duration +} + +/// 로그인 방식 +public enum LoginMethod: String { + case apple = "Apple" + case google = "Google" + case kakao = "Kakao" +} + +extension LoginMethod { + init?(authPlatform: String) { + switch authPlatform.lowercased() { + case "apple": + self = .apple + case "google": + self = .google + case "kakao": + self = .kakao + default: + return nil + } + } +} diff --git a/Projects/CoreKit/Sources/Core/Analytics/DependencyValues+Analytics.swift b/Projects/CoreKit/Sources/Core/Analytics/DependencyValues+Analytics.swift new file mode 100644 index 00000000..034ac5b6 --- /dev/null +++ b/Projects/CoreKit/Sources/Core/Analytics/DependencyValues+Analytics.swift @@ -0,0 +1,51 @@ +import Dependencies +import Foundation + +/// Amplitude Client Protocol +public struct AmplitudeClient { + public var initialize: @Sendable (String, String?) -> Void + public var track: @Sendable (AnalyticsEvent) -> Void + public var setUserId: @Sendable (String) -> Void + public var setUserProperties: @Sendable ([String: Any]) -> Void + public var flush: @Sendable () -> Void + public var reset: @Sendable () -> Void +} + +public extension DependencyValues { + var amplitude: AmplitudeClient { + get { self[AmplitudeClientKey.self] } + set { self[AmplitudeClientKey.self] = newValue } + } +} + +private enum AmplitudeClientKey: DependencyKey { + static let liveValue = AmplitudeClient( + initialize: { apiKey, userId in + AmplitudeManager.shared.initialize(apiKey: apiKey, userId: userId) + }, + track: { event in + AmplitudeManager.shared.track(event) + }, + setUserId: { userId in + AmplitudeManager.shared.setUserId(userId) + }, + setUserProperties: { properties in + AmplitudeManager.shared.setUserProperties(properties) + }, + flush: { + AmplitudeManager.shared.flush() + }, + reset: { + AmplitudeManager.shared.reset() + } + ) + + static let testValue = AmplitudeClient( + initialize: { _, _ in }, + track: { _ in }, + setUserId: { _ in }, + setUserProperties: { _ in }, + flush: { }, + reset: { } + ) +} From 80b30fdc41f6170040aae2b81329c2544677d905 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=95?= Date: Tue, 25 Nov 2025 18:30:56 +0900 Subject: [PATCH 07/20] =?UTF-8?q?[feat]=20#208=20amplitude=20-=20=EC=95=B1?= =?UTF-8?q?=20=EC=8B=A4=ED=96=89=20=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/App/Sources/AppDelegate/AppDelegate.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Projects/App/Sources/AppDelegate/AppDelegate.swift b/Projects/App/Sources/AppDelegate/AppDelegate.swift index f72ea97e..309c8128 100644 --- a/Projects/App/Sources/AppDelegate/AppDelegate.swift +++ b/Projects/App/Sources/AppDelegate/AppDelegate.swift @@ -6,19 +6,25 @@ // import SwiftUI +import UIKit import ComposableArchitecture import Firebase import FirebaseMessaging import GoogleSignIn +import Dependencies final class AppDelegate: NSObject { + @Dependency(\.amplitude) + private var amplitude + let store = Store(initialState: AppDelegateFeature.State()) { AppDelegateFeature() } } //MARK: - UIApplicationDelegate extension AppDelegate: UIApplicationDelegate { + func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { if GIDSignIn.sharedInstance.handle(url) { return true } return false @@ -30,6 +36,14 @@ extension AppDelegate: UIApplicationDelegate { didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { self.store.send(.didFinishLaunching) + + // 운영체제 버전 (ex: "iOS 18.0.0") + let osVersion = "iOS \(UIDevice.current.systemVersion)" + + // 앱 번들 버전 (ex: "2.0.1") + let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" + + amplitude.track(.app_open(deviceOS: osVersion, appVersion: appVersion)) return true } From f21a0809aa5d17d9dd1cee1219421a65ae124dbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=95?= Date: Tue, 25 Nov 2025 18:32:40 +0900 Subject: [PATCH 08/20] =?UTF-8?q?[feat]=20#208=20amplitude=20-=20=EC=8A=A4?= =?UTF-8?q?=ED=94=8C=EB=9E=98=EC=8B=9C=20=ED=99=94=EB=A9=B4=20=EB=85=B8?= =?UTF-8?q?=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Feature/FeatureIntro/Sources/Splash/SplashFeature.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Projects/Feature/FeatureIntro/Sources/Splash/SplashFeature.swift b/Projects/Feature/FeatureIntro/Sources/Splash/SplashFeature.swift index 3f3cdcdd..4874c209 100644 --- a/Projects/Feature/FeatureIntro/Sources/Splash/SplashFeature.swift +++ b/Projects/Feature/FeatureIntro/Sources/Splash/SplashFeature.swift @@ -27,6 +27,9 @@ public struct SplashFeature { var keychain @Dependency(VersionClient.self) var versionClient + @Dependency(\.amplitude) + var amplitude + /// - State @ObservableState public struct State { @@ -103,6 +106,7 @@ private extension SplashFeature { case .onAppear: return .run { [isNeedSessionDeleted = state.isNeedSessionDeleted] send in + amplitude.track(.view_splash) try await self.clock.sleep(for: .milliseconds(2000)) /// Version Check let response = try await versionClient.버전체크().toDomain() From 2cfce98479e5a064614924697479ad21bf0376b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=95?= Date: Tue, 25 Nov 2025 18:35:20 +0900 Subject: [PATCH 09/20] =?UTF-8?q?[feat]=20#208=20amplitude=20-=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=8B=9C=EB=8F=84,=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Feature/FeatureLogin/Sources/Login/LoginFeature.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Projects/Feature/FeatureLogin/Sources/Login/LoginFeature.swift b/Projects/Feature/FeatureLogin/Sources/Login/LoginFeature.swift index edc5e139..ce591cc9 100644 --- a/Projects/Feature/FeatureLogin/Sources/Login/LoginFeature.swift +++ b/Projects/Feature/FeatureLogin/Sources/Login/LoginFeature.swift @@ -23,6 +23,8 @@ public struct LoginFeature { var userDefaults @Dependency(KeychainClient.self) var keychain + @Dependency(\.amplitude) + var amplitude /// - State @ObservableState public struct State { @@ -108,8 +110,10 @@ private extension LoginFeature { func handleViewAction(_ action: Action.View, state: inout State) -> Effect { switch action { case .애플로그인_버튼_눌렀을때: + amplitude.track(.login_start(method: .apple)) return .send(.async(.애플로그인_소셜_API)) case .구글로그인_버튼_눌렀을때: + amplitude.track(.login_start(method: .google)) return .send(.async(.구글로그인_소셜_API)) } } @@ -169,7 +173,7 @@ private extension LoginFeature { let appleTokenRequest = AppleTokenRequest(authCode: authCode, jwt: jwt) let appleTokenResponse = try await authClient.apple(appleTokenRequest) keychain.save(.serverRefresh, appleTokenResponse.refresh_token) - + amplitude.track(.login_complete(method: .apple)) await send(.inner(.로그인_이후_화면이동(isRegistered: tokenResponse.isRegistered))) } case let .구글로그인_API(response): @@ -185,6 +189,7 @@ private extension LoginFeature { keychain.save(.accessToken, tokenResponse.accessToken) keychain.save(.refreshToken, tokenResponse.refreshToken) keychain.save(.serverRefresh, response.serverRefreshToken) + amplitude.track(.login_complete(method: .google)) await send(.inner(.로그인_이후_화면이동(isRegistered: tokenResponse.isRegistered))) } From bffdacca48b90fa878c2f6223c281dcadf551d4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=95?= Date: Tue, 25 Nov 2025 18:38:01 +0900 Subject: [PATCH 10/20] =?UTF-8?q?[feat]=20#208=20amplitude=20-=20=EC=98=A8?= =?UTF-8?q?=EB=B3=B4=EB=94=A9=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FeatureLogin/Sources/SignUpDone/SignUpDoneFeature.swift | 6 ++++++ .../FeatureLogin/Sources/SignUpDone/SignUpDoneView.swift | 1 + 2 files changed, 7 insertions(+) diff --git a/Projects/Feature/FeatureLogin/Sources/SignUpDone/SignUpDoneFeature.swift b/Projects/Feature/FeatureLogin/Sources/SignUpDone/SignUpDoneFeature.swift index 4a9a50ff..6a510157 100644 --- a/Projects/Feature/FeatureLogin/Sources/SignUpDone/SignUpDoneFeature.swift +++ b/Projects/Feature/FeatureLogin/Sources/SignUpDone/SignUpDoneFeature.swift @@ -14,6 +14,8 @@ import Util public struct SignUpDoneFeature { /// - Dependency @Dependency(\.dismiss) var dismiss + @Dependency(\.amplitude) + var amplitude /// - State @ObservableState public struct State: Equatable { @@ -41,6 +43,7 @@ public struct SignUpDoneFeature { case 제목_나타났을때 case 폭죽_이미지_나타났을때 case 푸키_이미지_나타났을때 + case 뷰가_나타났을때 } public enum InnerAction: Equatable { case 없음 } public enum AsyncAction: Equatable { case 없음 } @@ -95,6 +98,9 @@ private extension SignUpDoneFeature { case .푸키_이미지_나타났을때: state.pookiIsAppear = true return .none + case .뷰가_나타났을때: + amplitude.track(.onboarding_complete) + return .none } } /// - Inner Effect diff --git a/Projects/Feature/FeatureLogin/Sources/SignUpDone/SignUpDoneView.swift b/Projects/Feature/FeatureLogin/Sources/SignUpDone/SignUpDoneView.swift index 018e4360..8c47530d 100644 --- a/Projects/Feature/FeatureLogin/Sources/SignUpDone/SignUpDoneView.swift +++ b/Projects/Feature/FeatureLogin/Sources/SignUpDone/SignUpDoneView.swift @@ -49,6 +49,7 @@ public extension SignUpDoneView { } .ignoresSafeArea(edges: .bottom) .navigationBarBackButtonHidden() + .onAppear { send(.뷰가_나타났을때) } } } } From 8832cb4509719fdd8abc9c56b744ef8f691666fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=95?= Date: Tue, 25 Nov 2025 18:39:55 +0900 Subject: [PATCH 11/20] =?UTF-8?q?[feat]=20#208=20amplitude=20-=20=EA=B4=80?= =?UTF-8?q?=EC=8B=AC=EC=82=AC=20=EC=84=A0=ED=83=9D=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FeatureLogin/Sources/SelectField/SelectFieldFeature.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Projects/Feature/FeatureLogin/Sources/SelectField/SelectFieldFeature.swift b/Projects/Feature/FeatureLogin/Sources/SelectField/SelectFieldFeature.swift index 38771fd6..7fa8b6ba 100644 --- a/Projects/Feature/FeatureLogin/Sources/SelectField/SelectFieldFeature.swift +++ b/Projects/Feature/FeatureLogin/Sources/SelectField/SelectFieldFeature.swift @@ -14,6 +14,8 @@ public struct SelectFieldFeature { /// - Dependency @Dependency(\.dismiss) var dismiss @Dependency(UserClient.self) var userClient + @Dependency(\.amplitude) + var amplitude /// - State @ObservableState public struct State: Equatable { @@ -93,6 +95,7 @@ private extension SelectFieldFeature { } case .nextButtonTapped: let interests = Array(state.selectedFields) + amplitude.track(.interest_select(interests: interests)) return .send(.delegate(.pushSignUpDoneView(interests: interests))) case .backButtonTapped: return .run { _ in await self.dismiss() } From de4cec7737a83a2b94cac2d43e7eb791ac7df6d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=95?= Date: Tue, 25 Nov 2025 18:46:15 +0900 Subject: [PATCH 12/20] =?UTF-8?q?[feat]=20#208=20amplitude=20-=20=ED=8F=AC?= =?UTF-8?q?=ED=82=B7,=20=EC=B6=94=EC=B2=9C=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=20=EC=A7=84=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/Sources/MainTab/MainTabFeature.swift | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Projects/App/Sources/MainTab/MainTabFeature.swift b/Projects/App/Sources/MainTab/MainTabFeature.swift index 186d6bda..9e4695ea 100644 --- a/Projects/App/Sources/MainTab/MainTabFeature.swift +++ b/Projects/App/Sources/MainTab/MainTabFeature.swift @@ -24,6 +24,9 @@ public struct MainTabFeature { private var categoryClient @Dependency(UserDefaultsClient.self) private var userDefaults + @Dependency(\.amplitude) + private var amplitude + /// - State @ObservableState public struct State: Equatable { @@ -103,6 +106,14 @@ public struct MainTabFeature { guard state.linkPopup == nil else { return .none } state.categoryOfSavedContent = nil return .none + case .binding(\.selectedTab): + switch state.selectedTab { + case .pokit: + amplitude.track(.view_home_pokit(entryPoint: "pokit")) + case .recommend: + amplitude.track(.view_home_recommend(entryPoint: "recommend")) + } + return .none case .binding: return .none case let .pushAlertTapped(isTapped): @@ -197,6 +208,13 @@ private extension MainTabFeature { let categoryIdString = queryItems.first(where: { $0.name == "categoryId" })?.value, let categoryId = Int(categoryIdString) else { return .none } + + switch state.selectedTab { + case .pokit: + amplitude.track(.view_home_pokit(entryPoint: "deeplink")) + case .recommend: + amplitude.track(.view_home_recommend(entryPoint: "deeplink")) + } return .send(.async(.공유받은_카테고리_조회(categoryId: categoryId))) case .경고_확인버튼_클릭: From 9f475f4d2a7aa4c63767947739fe170a99341da8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=95?= Date: Tue, 25 Nov 2025 18:53:44 +0900 Subject: [PATCH 13/20] =?UTF-8?q?[feat]=20#208=20amplitude=20-=20=ED=8F=B4?= =?UTF-8?q?=EB=8D=94=20=EC=83=9D=EC=84=B1,=20=EB=A7=81=ED=81=AC=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Network/Category/CategoryClient+LiveKey.swift | 6 +++++- .../Data/Network/Content/ContentClient+LiveKey.swift | 11 +++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient+LiveKey.swift b/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient+LiveKey.swift index 05fc56e0..64611eec 100644 --- a/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient+LiveKey.swift +++ b/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient+LiveKey.swift @@ -12,6 +12,7 @@ import Moya extension CategoryClient: DependencyKey { public static let liveValue: Self = { + @Dependency(\.amplitude.track) var amplitudeTrack let provider = MoyaProvider.build() return Self( @@ -31,7 +32,10 @@ extension CategoryClient: DependencyKey { ) }, 카테고리_생성: { model in - try await provider.request(.카테고리생성(model: model)) + let response: CategoryEditResponse + response = try await provider.request(.카테고리생성(model: model)) + amplitudeTrack(.add_folder(folderName: response.categoryName)) + return response }, 카테고리_프로필_목록_조회: { try await provider.request(.카테고리_프로필_목록_조회) diff --git a/Projects/CoreKit/Sources/Data/Network/Content/ContentClient+LiveKey.swift b/Projects/CoreKit/Sources/Data/Network/Content/ContentClient+LiveKey.swift index 27db10d0..757b37be 100644 --- a/Projects/CoreKit/Sources/Data/Network/Content/ContentClient+LiveKey.swift +++ b/Projects/CoreKit/Sources/Data/Network/Content/ContentClient+LiveKey.swift @@ -10,8 +10,9 @@ import Moya extension ContentClient: DependencyKey { public static let liveValue: Self = { + @Dependency(\.amplitude.track) var amplitudeTrack let provider = MoyaProvider.build() - + return Self( 컨텐츠_삭제: { id in try await provider.requestNoBody(.컨텐츠_삭제(contentId: id)) @@ -23,7 +24,13 @@ extension ContentClient: DependencyKey { try await provider.request(.컨텐츠_수정(contentId: id, model: model)) }, 컨텐츠_추가: { model in - try await provider.request(.컨텐츠_추가(model: model)) + let response: ContentDetailResponse + response = try await provider.request(.컨텐츠_추가(model: model)) + amplitudeTrack(.add_link( + folderId: "\(response.category.categoryId)", + linkDomain: response.data + )) + return response }, 즐겨찾기: { id in try await provider.request(.즐겨찾기(contentId: id)) From 0f05a0f40fc4e3f77d8eacd56bf9dcbc38294b50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=95?= Date: Tue, 25 Nov 2025 18:57:31 +0900 Subject: [PATCH 14/20] =?UTF-8?q?[feat]=20#208=20amplitude=20-=20=EB=A7=81?= =?UTF-8?q?=ED=81=AC,=20=ED=8F=B4=EB=8D=94=20=EC=83=81=EC=84=B8=EB=B3=B4?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Data/Network/Category/CategoryClient+LiveKey.swift | 3 ++- .../Data/Network/Content/ContentClient+LiveKey.swift | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient+LiveKey.swift b/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient+LiveKey.swift index 64611eec..d6fe994a 100644 --- a/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient+LiveKey.swift +++ b/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient+LiveKey.swift @@ -44,7 +44,8 @@ extension CategoryClient: DependencyKey { try await provider.request(.유저_카테고리_개수_조회) }, 카테고리_상세_조회: { id in - try await provider.request(.카테고리_상세_조회(categoryId: id)) + amplitudeTrack(.view_folder_detail(folderId: id)) + return try await provider.request(.카테고리_상세_조회(categoryId: id)) }, 공유받은_카테고리_조회: { id, model in try await provider.request(.공유받은_카테고리_조회(categoryId: id, model: model)) diff --git a/Projects/CoreKit/Sources/Data/Network/Content/ContentClient+LiveKey.swift b/Projects/CoreKit/Sources/Data/Network/Content/ContentClient+LiveKey.swift index 757b37be..ede2bf2c 100644 --- a/Projects/CoreKit/Sources/Data/Network/Content/ContentClient+LiveKey.swift +++ b/Projects/CoreKit/Sources/Data/Network/Content/ContentClient+LiveKey.swift @@ -18,7 +18,13 @@ extension ContentClient: DependencyKey { try await provider.requestNoBody(.컨텐츠_삭제(contentId: id)) }, 컨텐츠_상세_조회: { id in - try await provider.request(.컨텐츠_상세_조회(contentId: id)) + let response: ContentDetailResponse + response = try await provider.request(.컨텐츠_상세_조회(contentId: id)) + amplitudeTrack(.view_link_detail( + linkId: "\(id)", + linkDomain: response.data + )) + return response }, 컨텐츠_수정: { id, model in try await provider.request(.컨텐츠_수정(contentId: id, model: model)) From 0c63176c0966e0e192409932a76f16f254beb3a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=95?= Date: Tue, 25 Nov 2025 19:11:52 +0900 Subject: [PATCH 15/20] =?UTF-8?q?[feat]=20#208=20amplitude=20-=20=EA=B3=B5?= =?UTF-8?q?=EC=9C=A0=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/CategoryDetailFeature.swift | 7 +++++++ .../Sources/ContentSetting/ContentSettingFeature.swift | 3 +++ 2 files changed, 10 insertions(+) diff --git a/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift b/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift index 9b3a9c24..a94ac92a 100644 --- a/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift +++ b/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift @@ -26,6 +26,9 @@ public struct CategoryDetailFeature { private var contentClient @Dependency(KakaoShareClient.self) private var kakaoShareClient + @Dependency(\.amplitude.track) + private var amplitudeTrack + /// - State @ObservableState public struct State: Equatable { @@ -212,6 +215,10 @@ private extension CategoryDetailFeature { ) case .공유_버튼_눌렀을때: + amplitudeTrack(.share_link( + linkId: "\(state.domain.category.id)", + shareTarget: "kakaotalk" + )) kakaoShareClient.카테고리_카카오톡_공유( CategoryKaKaoShareModel( categoryName: state.domain.category.categoryName, diff --git a/Projects/Feature/FeatureContentSetting/Sources/ContentSetting/ContentSettingFeature.swift b/Projects/Feature/FeatureContentSetting/Sources/ContentSetting/ContentSettingFeature.swift index a10909a1..d9d3a951 100644 --- a/Projects/Feature/FeatureContentSetting/Sources/ContentSetting/ContentSettingFeature.swift +++ b/Projects/Feature/FeatureContentSetting/Sources/ContentSetting/ContentSettingFeature.swift @@ -28,6 +28,9 @@ public struct ContentSettingFeature { private var categoryClient @Dependency(KeyboardClient.self) private var keyboardClient + @Dependency(\.amplitude.track) + private var amplitudeTrack + /// - State @ObservableState public struct State: Equatable { From 1d8e15e85f1e4138e910738bf987ebc89f5b518a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=95?= Date: Tue, 25 Nov 2025 19:36:26 +0900 Subject: [PATCH 16/20] =?UTF-8?q?[feat]=20#208=20amplitude=20-=20=EB=A7=81?= =?UTF-8?q?=ED=81=AC=20=EC=83=81=EC=84=B8,=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Core/Analytics/AnalyticsEvent.swift | 41 ++++++++++++++++--- .../Content/ContentClient+LiveKey.swift | 8 +--- .../ContentCard/ContentCardFeature.swift | 9 +++- .../ContentSettingFeature.swift | 6 ++- .../Sources/Recommend/RecommendFeature.swift | 37 ++++++++++++----- .../Sources/Recommend/RecommendView.swift | 2 +- 6 files changed, 77 insertions(+), 26 deletions(-) diff --git a/Projects/CoreKit/Sources/Core/Analytics/AnalyticsEvent.swift b/Projects/CoreKit/Sources/Core/Analytics/AnalyticsEvent.swift index b877f1a6..7891168d 100644 --- a/Projects/CoreKit/Sources/Core/Analytics/AnalyticsEvent.swift +++ b/Projects/CoreKit/Sources/Core/Analytics/AnalyticsEvent.swift @@ -11,9 +11,9 @@ public enum AnalyticsEvent { case view_home_pokit(entryPoint: String) case view_home_recommend(entryPoint: String) case add_folder(folderName: String) - case add_link(folderId: String, linkDomain: String) + case add_link(folderId: String, linkDomain: String, entryPoint: String? = nil, linkId: String? = nil, positionIndex: Int? = nil, algoVersion: String? = nil) case view_folder_detail(folderId: String) - case view_link_detail(linkId: String, linkDomain: String) + case view_link_detail(linkId: String, linkDomain: String, entryPoint: String? = nil, positionIndex: Int? = nil, cardType: String? = nil, algoVersion: String? = nil) case share_link(linkId: String, shareTarget: String) case session_end(duration: Int) @@ -70,20 +70,46 @@ public enum AnalyticsEvent { case let .add_folder(folderName): return [PropertyKey.folder_name.rawValue: folderName] - case let .add_link(folderId, linkDomain): - return [ + case let .add_link(folderId, linkDomain, entryPoint, linkId, positionIndex, algoVersion): + var props: [String: Any] = [ PropertyKey.folder_id.rawValue: folderId, PropertyKey.link_domain.rawValue: linkDomain ] + if let entryPoint = entryPoint { + props[PropertyKey.entry_point.rawValue] = entryPoint + } + if let linkId = linkId { + props[PropertyKey.link_id.rawValue] = linkId + } + if let positionIndex = positionIndex { + props[PropertyKey.position_index.rawValue] = positionIndex + } + if let algoVersion = algoVersion { + props[PropertyKey.algo_version.rawValue] = algoVersion + } + return props case let .view_folder_detail(folderId): return [PropertyKey.folder_id.rawValue: folderId] - case let .view_link_detail(linkId, linkDomain): - return [ + case let .view_link_detail(linkId, linkDomain, entryPoint, positionIndex, cardType, algoVersion): + var props: [String: Any] = [ PropertyKey.link_id.rawValue: linkId, PropertyKey.link_domain.rawValue: linkDomain ] + if let entryPoint = entryPoint { + props[PropertyKey.entry_point.rawValue] = entryPoint + } + if let positionIndex = positionIndex { + props[PropertyKey.position_index.rawValue] = positionIndex + } + if let cardType = cardType { + props[PropertyKey.card_type.rawValue] = cardType + } + if let algoVersion = algoVersion { + props[PropertyKey.algo_version.rawValue] = algoVersion + } + return props case let .share_link(linkId, shareTarget): return [ @@ -110,6 +136,9 @@ public enum PropertyKey: String { case link_id case share_target case duration + case position_index + case card_type + case algo_version } /// 로그인 방식 diff --git a/Projects/CoreKit/Sources/Data/Network/Content/ContentClient+LiveKey.swift b/Projects/CoreKit/Sources/Data/Network/Content/ContentClient+LiveKey.swift index ede2bf2c..757b37be 100644 --- a/Projects/CoreKit/Sources/Data/Network/Content/ContentClient+LiveKey.swift +++ b/Projects/CoreKit/Sources/Data/Network/Content/ContentClient+LiveKey.swift @@ -18,13 +18,7 @@ extension ContentClient: DependencyKey { try await provider.requestNoBody(.컨텐츠_삭제(contentId: id)) }, 컨텐츠_상세_조회: { id in - let response: ContentDetailResponse - response = try await provider.request(.컨텐츠_상세_조회(contentId: id)) - amplitudeTrack(.view_link_detail( - linkId: "\(id)", - linkDomain: response.data - )) - return response + try await provider.request(.컨텐츠_상세_조회(contentId: id)) }, 컨텐츠_수정: { id, model in try await provider.request(.컨텐츠_수정(contentId: id, model: model)) diff --git a/Projects/Feature/FeatureContentCard/Sources/ContentCard/ContentCardFeature.swift b/Projects/Feature/FeatureContentCard/Sources/ContentCard/ContentCardFeature.swift index 12fd5eb3..fe5c081a 100644 --- a/Projects/Feature/FeatureContentCard/Sources/ContentCard/ContentCardFeature.swift +++ b/Projects/Feature/FeatureContentCard/Sources/ContentCard/ContentCardFeature.swift @@ -21,6 +21,9 @@ public struct ContentCardFeature { private var openURL @Dependency(ContentClient.self) private var contentClient + @Dependency(\.amplitude.track) + private var amplitudeTrack + /// - State @ObservableState public struct State: Equatable, Identifiable { @@ -113,7 +116,11 @@ private extension ContentCardFeature { guard let url = URL(string: state.content.data) else { return .none } - return .run { send in + return .run { [content = state.content] send in + amplitudeTrack(.view_link_detail( + linkId: "\(content.id)", + linkDomain: content.data + )) await send(.async(.컨텐츠_상세_조회_API)) await openURL(url) } diff --git a/Projects/Feature/FeatureContentSetting/Sources/ContentSetting/ContentSettingFeature.swift b/Projects/Feature/FeatureContentSetting/Sources/ContentSetting/ContentSettingFeature.swift index d9d3a951..7b0bc337 100644 --- a/Projects/Feature/FeatureContentSetting/Sources/ContentSetting/ContentSettingFeature.swift +++ b/Projects/Feature/FeatureContentSetting/Sources/ContentSetting/ContentSettingFeature.swift @@ -444,7 +444,11 @@ private extension ContentSettingFeature { thumbNail: state.domain.thumbNail ) return .run { send in - let content = try await contentClient.컨텐츠_추가(request) + let response = try await contentClient.컨텐츠_추가(request) + amplitudeTrack(.add_link( + folderId: "\(response.contentId)", + linkDomain: response.data + )) await send(.inner(.선택한_포킷_인메모리_삭제)) await send(.delegate(.저장하기_완료(category: category))) } catch: { error, send in diff --git a/Projects/Feature/FeatureRecommend/Sources/Recommend/RecommendFeature.swift b/Projects/Feature/FeatureRecommend/Sources/Recommend/RecommendFeature.swift index df1ba34b..07d83460 100644 --- a/Projects/Feature/FeatureRecommend/Sources/Recommend/RecommendFeature.swift +++ b/Projects/Feature/FeatureRecommend/Sources/Recommend/RecommendFeature.swift @@ -23,6 +23,9 @@ public struct RecommendFeature { private var categoryClient @Dependency(\.openURL) private var openURL + @Dependency(\.amplitude.track) + private var amplitudeTrack + /// - State @ObservableState public struct State: Equatable { @@ -87,7 +90,7 @@ public struct RecommendFeature { case 링크_공유_완료되었을때 case 검색_버튼_눌렀을때 case 알림_버튼_눌렀을때 - case 추천_컨텐츠_눌렀을때(String) + case 추천_컨텐츠_눌렀을때(BaseContentItem) case 경고시트_dismiss case 포킷선택_항목_눌렀을때(pokit: BaseCategoryItem) case 포킷_추가하기_버튼_눌렀을때 @@ -211,8 +214,17 @@ private extension RecommendFeature { return .send(.delegate(.검색_버튼_눌렀을때)) case .알림_버튼_눌렀을때: return .send(.delegate(.알림_버튼_눌렀을때)) - case let .추천_컨텐츠_눌렀을때(urlString): - guard let url = URL(string: urlString) else { return .none } + case let .추천_컨텐츠_눌렀을때(content): + guard let url = URL(string: content.data) else { return .none } + let index = state.recommendedList?.index(id: content.id) + amplitudeTrack(.view_link_detail( + linkId: "\(content.id)", + linkDomain: content.data, + entryPoint: "recommend", + positionIndex: index, + cardType: "list", + algoVersion: "v1.2" + )) return .run { _ in await openURL(url) } case .관심사_편집_버튼_눌렀을때: state.showKeywordSheet = true @@ -355,12 +367,8 @@ private extension RecommendFeature { ) return categoryListFetch(request: request) case .컨텐츠_추가_API: - guard - let categoryId = state.selectedPokit?.id, - let category = state.domain.categoryListInQuiry.data?.first(where: { - $0.id == categoryId - }), - let content = state.addContent + guard let categoryId = state.selectedPokit?.id, + let content = state.addContent else { return .none } let request = ContentBaseRequest( data: content.data, @@ -370,8 +378,17 @@ private extension RecommendFeature { alertYn: "NO", thumbNail: content.thumbNail ) + let index = state.recommendedList?.index(id: content.id) return .run { send in - let content = try await contentClient.컨텐츠_추가(request) + let response = try await contentClient.컨텐츠_추가(request) + amplitudeTrack(.add_link( + folderId: "\(categoryId)", + linkDomain: content.data, + entryPoint: "recommend", + linkId: "\(response.contentId)", + positionIndex: index, + algoVersion: "v1.2" + )) await send(.delegate(.저장하기_완료)) } } diff --git a/Projects/Feature/FeatureRecommend/Sources/Recommend/RecommendView.swift b/Projects/Feature/FeatureRecommend/Sources/Recommend/RecommendView.swift index 509a264e..19a13ddc 100644 --- a/Projects/Feature/FeatureRecommend/Sources/Recommend/RecommendView.swift +++ b/Projects/Feature/FeatureRecommend/Sources/Recommend/RecommendView.swift @@ -196,7 +196,7 @@ private extension RecommendView { @ViewBuilder func recommendedCard(_ content: BaseContentItem) -> some View { - Button(action: { send(.추천_컨텐츠_눌렀을때(content.data)) }) { + Button(action: { send(.추천_컨텐츠_눌렀을때(content)) }) { recomendedCardLabel(content) } } From a2d34369bc7798a7fed951f8d1b4599856c9b855 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=95?= Date: Tue, 25 Nov 2025 19:37:20 +0900 Subject: [PATCH 17/20] =?UTF-8?q?[chore]=20#208=20amplitude=20=EB=A7=81?= =?UTF-8?q?=ED=81=AC=20=EC=A0=80=EC=9E=A5=20=ED=8A=B8=EB=A6=AC=EA=B1=B0=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Data/Network/Content/ContentClient+LiveKey.swift | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/Projects/CoreKit/Sources/Data/Network/Content/ContentClient+LiveKey.swift b/Projects/CoreKit/Sources/Data/Network/Content/ContentClient+LiveKey.swift index 757b37be..27db10d0 100644 --- a/Projects/CoreKit/Sources/Data/Network/Content/ContentClient+LiveKey.swift +++ b/Projects/CoreKit/Sources/Data/Network/Content/ContentClient+LiveKey.swift @@ -10,9 +10,8 @@ import Moya extension ContentClient: DependencyKey { public static let liveValue: Self = { - @Dependency(\.amplitude.track) var amplitudeTrack let provider = MoyaProvider.build() - + return Self( 컨텐츠_삭제: { id in try await provider.requestNoBody(.컨텐츠_삭제(contentId: id)) @@ -24,13 +23,7 @@ extension ContentClient: DependencyKey { try await provider.request(.컨텐츠_수정(contentId: id, model: model)) }, 컨텐츠_추가: { model in - let response: ContentDetailResponse - response = try await provider.request(.컨텐츠_추가(model: model)) - amplitudeTrack(.add_link( - folderId: "\(response.category.categoryId)", - linkDomain: response.data - )) - return response + try await provider.request(.컨텐츠_추가(model: model)) }, 즐겨찾기: { id in try await provider.request(.즐겨찾기(contentId: id)) From 6293faa58dc22cfb95a0b2e20a9c186dc7b67073 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=95?= Date: Tue, 25 Nov 2025 19:54:07 +0900 Subject: [PATCH 18/20] =?UTF-8?q?[feat]=20#208=20amplitude=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/App/Resources/Pokit-info.plist | 2 ++ Projects/App/Sources/AppDelegate/AppDelegate.swift | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Projects/App/Resources/Pokit-info.plist b/Projects/App/Resources/Pokit-info.plist index ff684a8a..a20747c6 100644 --- a/Projects/App/Resources/Pokit-info.plist +++ b/Projects/App/Resources/Pokit-info.plist @@ -94,5 +94,7 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + AMPLITUDE_API_KEY + $(AMPLITUDE_API_KEY) diff --git a/Projects/App/Sources/AppDelegate/AppDelegate.swift b/Projects/App/Sources/AppDelegate/AppDelegate.swift index 309c8128..cdae5ca3 100644 --- a/Projects/App/Sources/AppDelegate/AppDelegate.swift +++ b/Projects/App/Sources/AppDelegate/AppDelegate.swift @@ -42,7 +42,9 @@ extension AppDelegate: UIApplicationDelegate { // 앱 번들 버전 (ex: "2.0.1") let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" - + let amplitudeKey = Bundle.main.infoDictionary?["AMPLITUDE_API_KEY"] as? String ?? "" + amplitude.initialize(amplitudeKey, nil) + amplitude.track(.app_open(deviceOS: osVersion, appVersion: appVersion)) return true } From 71eada91858e772be012e7a52d5a00c049ec7ac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=95?= Date: Tue, 25 Nov 2025 19:54:34 +0900 Subject: [PATCH 19/20] =?UTF-8?q?[refactor]=20#208=20amplitude=20track=20?= =?UTF-8?q?=ED=98=B8=EC=B6=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/App/Sources/MainTab/MainTabFeature.swift | 12 ++++++------ .../FeatureIntro/Sources/Splash/SplashFeature.swift | 6 +++--- .../FeatureLogin/Sources/Login/LoginFeature.swift | 12 ++++++------ .../Sources/SelectField/SelectFieldFeature.swift | 6 +++--- .../Sources/SignUpDone/SignUpDoneFeature.swift | 6 +++--- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/Projects/App/Sources/MainTab/MainTabFeature.swift b/Projects/App/Sources/MainTab/MainTabFeature.swift index 9e4695ea..b6096ec8 100644 --- a/Projects/App/Sources/MainTab/MainTabFeature.swift +++ b/Projects/App/Sources/MainTab/MainTabFeature.swift @@ -24,8 +24,8 @@ public struct MainTabFeature { private var categoryClient @Dependency(UserDefaultsClient.self) private var userDefaults - @Dependency(\.amplitude) - private var amplitude + @Dependency(\.amplitude.track) + private var amplitudeTrack /// - State @ObservableState @@ -109,9 +109,9 @@ public struct MainTabFeature { case .binding(\.selectedTab): switch state.selectedTab { case .pokit: - amplitude.track(.view_home_pokit(entryPoint: "pokit")) + amplitudeTrack(.view_home_pokit(entryPoint: "pokit")) case .recommend: - amplitude.track(.view_home_recommend(entryPoint: "recommend")) + amplitudeTrack(.view_home_recommend(entryPoint: "recommend")) } return .none case .binding: @@ -211,9 +211,9 @@ private extension MainTabFeature { switch state.selectedTab { case .pokit: - amplitude.track(.view_home_pokit(entryPoint: "deeplink")) + amplitudeTrack(.view_home_pokit(entryPoint: "deeplink")) case .recommend: - amplitude.track(.view_home_recommend(entryPoint: "deeplink")) + amplitudeTrack(.view_home_recommend(entryPoint: "deeplink")) } return .send(.async(.공유받은_카테고리_조회(categoryId: categoryId))) diff --git a/Projects/Feature/FeatureIntro/Sources/Splash/SplashFeature.swift b/Projects/Feature/FeatureIntro/Sources/Splash/SplashFeature.swift index 4874c209..d8a53b53 100644 --- a/Projects/Feature/FeatureIntro/Sources/Splash/SplashFeature.swift +++ b/Projects/Feature/FeatureIntro/Sources/Splash/SplashFeature.swift @@ -27,8 +27,8 @@ public struct SplashFeature { var keychain @Dependency(VersionClient.self) var versionClient - @Dependency(\.amplitude) - var amplitude + @Dependency(\.amplitude.track) + private var amplitudeTrack /// - State @ObservableState @@ -106,7 +106,7 @@ private extension SplashFeature { case .onAppear: return .run { [isNeedSessionDeleted = state.isNeedSessionDeleted] send in - amplitude.track(.view_splash) + amplitudeTrack(.view_splash) try await self.clock.sleep(for: .milliseconds(2000)) /// Version Check let response = try await versionClient.버전체크().toDomain() diff --git a/Projects/Feature/FeatureLogin/Sources/Login/LoginFeature.swift b/Projects/Feature/FeatureLogin/Sources/Login/LoginFeature.swift index ce591cc9..c2ecb014 100644 --- a/Projects/Feature/FeatureLogin/Sources/Login/LoginFeature.swift +++ b/Projects/Feature/FeatureLogin/Sources/Login/LoginFeature.swift @@ -23,8 +23,8 @@ public struct LoginFeature { var userDefaults @Dependency(KeychainClient.self) var keychain - @Dependency(\.amplitude) - var amplitude + @Dependency(\.amplitude.track) + private var amplitudeTrack /// - State @ObservableState public struct State { @@ -110,10 +110,10 @@ private extension LoginFeature { func handleViewAction(_ action: Action.View, state: inout State) -> Effect { switch action { case .애플로그인_버튼_눌렀을때: - amplitude.track(.login_start(method: .apple)) + amplitudeTrack(.login_start(method: .apple)) return .send(.async(.애플로그인_소셜_API)) case .구글로그인_버튼_눌렀을때: - amplitude.track(.login_start(method: .google)) + amplitudeTrack(.login_start(method: .google)) return .send(.async(.구글로그인_소셜_API)) } } @@ -173,7 +173,7 @@ private extension LoginFeature { let appleTokenRequest = AppleTokenRequest(authCode: authCode, jwt: jwt) let appleTokenResponse = try await authClient.apple(appleTokenRequest) keychain.save(.serverRefresh, appleTokenResponse.refresh_token) - amplitude.track(.login_complete(method: .apple)) + amplitudeTrack(.login_complete(method: .apple)) await send(.inner(.로그인_이후_화면이동(isRegistered: tokenResponse.isRegistered))) } case let .구글로그인_API(response): @@ -189,7 +189,7 @@ private extension LoginFeature { keychain.save(.accessToken, tokenResponse.accessToken) keychain.save(.refreshToken, tokenResponse.refreshToken) keychain.save(.serverRefresh, response.serverRefreshToken) - amplitude.track(.login_complete(method: .google)) + amplitudeTrack(.login_complete(method: .google)) await send(.inner(.로그인_이후_화면이동(isRegistered: tokenResponse.isRegistered))) } diff --git a/Projects/Feature/FeatureLogin/Sources/SelectField/SelectFieldFeature.swift b/Projects/Feature/FeatureLogin/Sources/SelectField/SelectFieldFeature.swift index 7fa8b6ba..fa1e5e6f 100644 --- a/Projects/Feature/FeatureLogin/Sources/SelectField/SelectFieldFeature.swift +++ b/Projects/Feature/FeatureLogin/Sources/SelectField/SelectFieldFeature.swift @@ -14,8 +14,8 @@ public struct SelectFieldFeature { /// - Dependency @Dependency(\.dismiss) var dismiss @Dependency(UserClient.self) var userClient - @Dependency(\.amplitude) - var amplitude + @Dependency(\.amplitude.track) + private var amplitudeTrack /// - State @ObservableState public struct State: Equatable { @@ -95,7 +95,7 @@ private extension SelectFieldFeature { } case .nextButtonTapped: let interests = Array(state.selectedFields) - amplitude.track(.interest_select(interests: interests)) + amplitudeTrack(.interest_select(interests: interests)) return .send(.delegate(.pushSignUpDoneView(interests: interests))) case .backButtonTapped: return .run { _ in await self.dismiss() } diff --git a/Projects/Feature/FeatureLogin/Sources/SignUpDone/SignUpDoneFeature.swift b/Projects/Feature/FeatureLogin/Sources/SignUpDone/SignUpDoneFeature.swift index 6a510157..b0429803 100644 --- a/Projects/Feature/FeatureLogin/Sources/SignUpDone/SignUpDoneFeature.swift +++ b/Projects/Feature/FeatureLogin/Sources/SignUpDone/SignUpDoneFeature.swift @@ -14,8 +14,8 @@ import Util public struct SignUpDoneFeature { /// - Dependency @Dependency(\.dismiss) var dismiss - @Dependency(\.amplitude) - var amplitude + @Dependency(\.amplitude.track) + private var amplitudeTrack /// - State @ObservableState public struct State: Equatable { @@ -99,7 +99,7 @@ private extension SignUpDoneFeature { state.pookiIsAppear = true return .none case .뷰가_나타났을때: - amplitude.track(.onboarding_complete) + amplitudeTrack(.onboarding_complete) return .none } } From e67dd024ab9bf0613ed33e05cdfe867712ca1895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=95?= Date: Tue, 25 Nov 2025 20:15:51 +0900 Subject: [PATCH 20/20] =?UTF-8?q?[feat]=20#208=20amplitude=20-=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=20=EC=A0=95=EB=B3=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/Sources/AppDelegate/AppDelegate.swift | 1 - .../Network/User/UserClient+LiveKey.swift | 7 ++++++- .../Sources/Splash/SplashFeature.swift | 12 +++++++++--- .../Sources/Login/LoginFeature.swift | 19 +++++++++++++------ 4 files changed, 28 insertions(+), 11 deletions(-) diff --git a/Projects/App/Sources/AppDelegate/AppDelegate.swift b/Projects/App/Sources/AppDelegate/AppDelegate.swift index cdae5ca3..c51a2024 100644 --- a/Projects/App/Sources/AppDelegate/AppDelegate.swift +++ b/Projects/App/Sources/AppDelegate/AppDelegate.swift @@ -44,7 +44,6 @@ extension AppDelegate: UIApplicationDelegate { let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" let amplitudeKey = Bundle.main.infoDictionary?["AMPLITUDE_API_KEY"] as? String ?? "" amplitude.initialize(amplitudeKey, nil) - amplitude.track(.app_open(deviceOS: osVersion, appVersion: appVersion)) return true } diff --git a/Projects/CoreKit/Sources/Data/Network/User/UserClient+LiveKey.swift b/Projects/CoreKit/Sources/Data/Network/User/UserClient+LiveKey.swift index 900ec4d1..415be9bb 100644 --- a/Projects/CoreKit/Sources/Data/Network/User/UserClient+LiveKey.swift +++ b/Projects/CoreKit/Sources/Data/Network/User/UserClient+LiveKey.swift @@ -12,6 +12,8 @@ import Moya extension UserClient: DependencyKey { public static let liveValue: Self = { + @Dependency(\.amplitude.setUserProperties) + var amplitudeSetUserProperties let provider = MoyaProvider.build() return Self( @@ -22,7 +24,10 @@ extension UserClient: DependencyKey { try await provider.request(.닉네임_수정(model: model)) }, 회원등록: { model in - try await provider.request(.회원등록(model: model)) + let response: BaseUserResponse + response = try await provider.request(.회원등록(model: model)) + amplitudeSetUserProperties(["userId": response.id]) + return response }, 닉네임_중복_체크: { nickname in try await provider.request(.닉네임_중복_체크(nickname: nickname)) diff --git a/Projects/Feature/FeatureIntro/Sources/Splash/SplashFeature.swift b/Projects/Feature/FeatureIntro/Sources/Splash/SplashFeature.swift index d8a53b53..c06021fa 100644 --- a/Projects/Feature/FeatureIntro/Sources/Splash/SplashFeature.swift +++ b/Projects/Feature/FeatureIntro/Sources/Splash/SplashFeature.swift @@ -27,8 +27,10 @@ public struct SplashFeature { var keychain @Dependency(VersionClient.self) var versionClient - @Dependency(\.amplitude.track) - private var amplitudeTrack + @Dependency(\.amplitude) + private var amplitude + @Dependency(UserClient.self) + var userClient /// - State @ObservableState @@ -106,7 +108,7 @@ private extension SplashFeature { case .onAppear: return .run { [isNeedSessionDeleted = state.isNeedSessionDeleted] send in - amplitudeTrack(.view_splash) + amplitude.track(.view_splash) try await self.clock.sleep(for: .milliseconds(2000)) /// Version Check let response = try await versionClient.버전체크().toDomain() @@ -177,6 +179,10 @@ private extension SplashFeature { let tokenRequest = ReissueRequest(refreshToken: refreshToken) let tokenResponse = try await authClient.토큰재발급(tokenRequest) keychain.save(.accessToken, tokenResponse.accessToken) + + let user = try await userClient.닉네임_조회() + amplitude.setUserProperties(["userId": user.id]) + await send(.delegate(.autoLoginSuccess)) } catch { await send(.delegate(.loginNeeded), animation: .smooth) diff --git a/Projects/Feature/FeatureLogin/Sources/Login/LoginFeature.swift b/Projects/Feature/FeatureLogin/Sources/Login/LoginFeature.swift index c2ecb014..c7cec094 100644 --- a/Projects/Feature/FeatureLogin/Sources/Login/LoginFeature.swift +++ b/Projects/Feature/FeatureLogin/Sources/Login/LoginFeature.swift @@ -23,8 +23,8 @@ public struct LoginFeature { var userDefaults @Dependency(KeychainClient.self) var keychain - @Dependency(\.amplitude.track) - private var amplitudeTrack + @Dependency(\.amplitude) + private var amplitude /// - State @ObservableState public struct State { @@ -110,10 +110,10 @@ private extension LoginFeature { func handleViewAction(_ action: Action.View, state: inout State) -> Effect { switch action { case .애플로그인_버튼_눌렀을때: - amplitudeTrack(.login_start(method: .apple)) + amplitude.track(.login_start(method: .apple)) return .send(.async(.애플로그인_소셜_API)) case .구글로그인_버튼_눌렀을때: - amplitudeTrack(.login_start(method: .google)) + amplitude.track(.login_start(method: .google)) return .send(.async(.구글로그인_소셜_API)) } } @@ -173,7 +173,11 @@ private extension LoginFeature { let appleTokenRequest = AppleTokenRequest(authCode: authCode, jwt: jwt) let appleTokenResponse = try await authClient.apple(appleTokenRequest) keychain.save(.serverRefresh, appleTokenResponse.refresh_token) - amplitudeTrack(.login_complete(method: .apple)) + amplitude.track(.login_complete(method: .apple)) + + let user = try await userClient.닉네임_조회() + amplitude.setUserProperties(["userId": user.id]) + await send(.inner(.로그인_이후_화면이동(isRegistered: tokenResponse.isRegistered))) } case let .구글로그인_API(response): @@ -189,7 +193,10 @@ private extension LoginFeature { keychain.save(.accessToken, tokenResponse.accessToken) keychain.save(.refreshToken, tokenResponse.refreshToken) keychain.save(.serverRefresh, response.serverRefreshToken) - amplitudeTrack(.login_complete(method: .google)) + amplitude.track(.login_complete(method: .google)) + + let user = try await userClient.닉네임_조회() + amplitude.setUserProperties(["userId": user.id]) await send(.inner(.로그인_이후_화면이동(isRegistered: tokenResponse.isRegistered))) }