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")
]
)