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 f72ea97e..c51a2024 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,15 @@ 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 ?? "" + 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/App/Sources/MainTab/MainTabFeature.swift b/Projects/App/Sources/MainTab/MainTabFeature.swift index 186d6bda..b6096ec8 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.track) + private var amplitudeTrack + /// - 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: + amplitudeTrack(.view_home_pokit(entryPoint: "pokit")) + case .recommend: + amplitudeTrack(.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: + amplitudeTrack(.view_home_pokit(entryPoint: "deeplink")) + case .recommend: + amplitudeTrack(.view_home_recommend(entryPoint: "deeplink")) + } return .send(.async(.공유받은_카테고리_조회(categoryId: categoryId))) case .경고_확인버튼_클릭: 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) } } } 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..7891168d --- /dev/null +++ b/Projects/CoreKit/Sources/Core/Analytics/AnalyticsEvent.swift @@ -0,0 +1,164 @@ +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, 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, entryPoint: String? = nil, positionIndex: Int? = nil, cardType: String? = nil, algoVersion: String? = nil) + 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, 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, 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 [ + 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 + case position_index + case card_type + case algo_version +} + +/// 로그인 방식 +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: { } + ) +} diff --git a/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient+LiveKey.swift b/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient+LiveKey.swift index 05fc56e0..d6fe994a 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(.카테고리_프로필_목록_조회) @@ -40,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/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/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() 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/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) 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 a10909a1..7b0bc337 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 { @@ -441,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/FeatureIntro/Sources/Splash/SplashFeature.swift b/Projects/Feature/FeatureIntro/Sources/Splash/SplashFeature.swift index 3f3cdcdd..c06021fa 100644 --- a/Projects/Feature/FeatureIntro/Sources/Splash/SplashFeature.swift +++ b/Projects/Feature/FeatureIntro/Sources/Splash/SplashFeature.swift @@ -27,6 +27,11 @@ public struct SplashFeature { var keychain @Dependency(VersionClient.self) var versionClient + @Dependency(\.amplitude) + private var amplitude + @Dependency(UserClient.self) + var userClient + /// - State @ObservableState public struct State { @@ -103,6 +108,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() @@ -173,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 edc5e139..c7cec094 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) + private 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,6 +173,10 @@ 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)) + + let user = try await userClient.닉네임_조회() + amplitude.setUserProperties(["userId": user.id]) await send(.inner(.로그인_이후_화면이동(isRegistered: tokenResponse.isRegistered))) } @@ -185,6 +193,10 @@ private extension LoginFeature { keychain.save(.accessToken, tokenResponse.accessToken) keychain.save(.refreshToken, tokenResponse.refreshToken) keychain.save(.serverRefresh, response.serverRefreshToken) + amplitude.track(.login_complete(method: .google)) + + let user = try await userClient.닉네임_조회() + amplitude.setUserProperties(["userId": user.id]) 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 38771fd6..fa1e5e6f 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.track) + private var amplitudeTrack /// - State @ObservableState public struct State: Equatable { @@ -93,6 +95,7 @@ private extension SelectFieldFeature { } case .nextButtonTapped: let interests = Array(state.selectedFields) + 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 4a9a50ff..b0429803 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.track) + private var amplitudeTrack /// - 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 .뷰가_나타났을때: + amplitudeTrack(.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(.뷰가_나타났을때) } } } } 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) } } 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( "프로필 편집", 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") ] )