Skip to content

Commit b189c9d

Browse files
authored
Merge pull request #209 from YAPP-Github/feat/#208-2.0.8
Feat/#208 2.0.8
2 parents 6e0ff82 + e67dd02 commit b189c9d

File tree

24 files changed

+444
-24
lines changed

24 files changed

+444
-24
lines changed

Projects/App/Resources/Pokit-info.plist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,5 +94,7 @@
9494
<string>UIInterfaceOrientationLandscapeLeft</string>
9595
<string>UIInterfaceOrientationLandscapeRight</string>
9696
</array>
97+
<key>AMPLITUDE_API_KEY</key>
98+
<string>$(AMPLITUDE_API_KEY)</string>
9799
</dict>
98100
</plist>

Projects/App/Sources/AppDelegate/AppDelegate.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,25 @@
66
//
77

88
import SwiftUI
9+
import UIKit
910

1011
import ComposableArchitecture
1112
import Firebase
1213
import FirebaseMessaging
1314
import GoogleSignIn
15+
import Dependencies
1416

1517
final class AppDelegate: NSObject {
18+
@Dependency(\.amplitude)
19+
private var amplitude
20+
1621
let store = Store(initialState: AppDelegateFeature.State()) {
1722
AppDelegateFeature()
1823
}
1924
}
2025
//MARK: - UIApplicationDelegate
2126
extension AppDelegate: UIApplicationDelegate {
27+
2228
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
2329
if GIDSignIn.sharedInstance.handle(url) { return true }
2430
return false
@@ -30,6 +36,15 @@ extension AppDelegate: UIApplicationDelegate {
3036
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
3137
) -> Bool {
3238
self.store.send(.didFinishLaunching)
39+
40+
// 운영체제 버전 (ex: "iOS 18.0.0")
41+
let osVersion = "iOS \(UIDevice.current.systemVersion)"
42+
43+
// 앱 번들 버전 (ex: "2.0.1")
44+
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
45+
let amplitudeKey = Bundle.main.infoDictionary?["AMPLITUDE_API_KEY"] as? String ?? ""
46+
amplitude.initialize(amplitudeKey, nil)
47+
amplitude.track(.app_open(deviceOS: osVersion, appVersion: appVersion))
3348
return true
3449
}
3550

Projects/App/Sources/MainTab/MainTabFeature.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ public struct MainTabFeature {
2424
private var categoryClient
2525
@Dependency(UserDefaultsClient.self)
2626
private var userDefaults
27+
@Dependency(\.amplitude.track)
28+
private var amplitudeTrack
29+
2730
/// - State
2831
@ObservableState
2932
public struct State: Equatable {
@@ -103,6 +106,14 @@ public struct MainTabFeature {
103106
guard state.linkPopup == nil else { return .none }
104107
state.categoryOfSavedContent = nil
105108
return .none
109+
case .binding(\.selectedTab):
110+
switch state.selectedTab {
111+
case .pokit:
112+
amplitudeTrack(.view_home_pokit(entryPoint: "pokit"))
113+
case .recommend:
114+
amplitudeTrack(.view_home_recommend(entryPoint: "recommend"))
115+
}
116+
return .none
106117
case .binding:
107118
return .none
108119
case let .pushAlertTapped(isTapped):
@@ -197,6 +208,13 @@ private extension MainTabFeature {
197208
let categoryIdString = queryItems.first(where: { $0.name == "categoryId" })?.value,
198209
let categoryId = Int(categoryIdString)
199210
else { return .none }
211+
212+
switch state.selectedTab {
213+
case .pokit:
214+
amplitudeTrack(.view_home_pokit(entryPoint: "deeplink"))
215+
case .recommend:
216+
amplitudeTrack(.view_home_recommend(entryPoint: "deeplink"))
217+
}
200218

201219
return .send(.async(.공유받은_카테고리_조회(categoryId: categoryId)))
202220
case .경고_확인버튼_클릭:

Projects/App/Sources/MainTab/MainTabFeatureView.swift

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,14 @@ private extension MainTabView {
269269

270270
var body: some View {
271271
GeometryReader { proxy in
272-
let bottomSafeArea = proxy.safeAreaInsets.bottom
272+
let bottomPadding: CGFloat = {
273+
if #available(iOS 26.0, *) {
274+
return 32
275+
} else {
276+
return 48
277+
}
278+
}()
279+
273280
HStack(spacing: 20) {
274281
Spacer()
275282

@@ -302,7 +309,7 @@ private extension MainTabView {
302309

303310
Spacer()
304311
}
305-
.padding(.bottom, 48 - bottomSafeArea)
312+
.padding(.bottom, bottomPadding)
306313
.padding(.top, 36)
307314
.pokitPresentationCornerRadius()
308315
.pokitPresentationBackground()
@@ -315,6 +322,7 @@ private extension MainTabView {
315322
}
316323
.presentationDetents([.height(self.height)])
317324
}
325+
.ignoresSafeArea(edges: .bottom)
318326
}
319327
}
320328
}

Projects/CoreKit/Project.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ let coreKit: Target = .target(
3636
.external(name: "KakaoSDKCommon"),
3737
.external(name: "KakaoSDKShare"),
3838
.external(name: "KakaoSDKTemplate"),
39+
.external(name: "AmplitudeSwift"),
3940
],
4041
settings: .settings()
4142
)
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import Foundation
2+
import AmplitudeSwift
3+
4+
/// Amplitude Analytics 관리자
5+
public final class AmplitudeManager {
6+
public static let shared = AmplitudeManager()
7+
8+
private var amplitude: Amplitude?
9+
10+
private init() {}
11+
12+
/// Amplitude 초기화
13+
/// - Parameters:
14+
/// - apiKey: Amplitude API Key
15+
/// - userId: 사용자 ID (옵셔널)
16+
public func initialize(apiKey: String, userId: String? = nil) {
17+
amplitude = Amplitude(configuration: Configuration(
18+
apiKey: apiKey
19+
))
20+
21+
if let userId = userId {
22+
setUserId(userId)
23+
}
24+
}
25+
26+
/// 사용자 ID 설정
27+
/// - Parameter userId: 사용자 ID
28+
public func setUserId(_ userId: String) {
29+
amplitude?.setUserId(userId: userId)
30+
}
31+
32+
/// 사용자 속성 설정
33+
/// - Parameter properties: 사용자 속성
34+
public func setUserProperties(_ properties: [String: Any]) {
35+
let identify = Identify()
36+
properties.forEach { key, value in
37+
identify.set(property: key, value: value)
38+
}
39+
amplitude?.identify(identify: identify)
40+
}
41+
42+
/// 이벤트 전송
43+
/// - Parameter event: AnalyticsEvent
44+
public func track(_ event: AnalyticsEvent) {
45+
guard let amplitude = amplitude else {
46+
print("⚠️ Amplitude가 초기화되지 않았습니다.")
47+
return
48+
}
49+
50+
let eventProperties = event.properties.isEmpty ? nil : event.properties
51+
amplitude.track(
52+
eventType: event.eventName,
53+
eventProperties: eventProperties
54+
)
55+
56+
#if DEBUG
57+
print("📊 [Analytics] \(event.eventName)")
58+
if let properties = eventProperties {
59+
print(" Properties: \(properties)")
60+
}
61+
#endif
62+
}
63+
64+
/// 이벤트 버퍼 즉시 전송
65+
public func flush() {
66+
amplitude?.flush()
67+
}
68+
69+
/// Amplitude 리셋 (로그아웃 시 사용)
70+
public func reset() {
71+
amplitude?.reset()
72+
}
73+
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import Foundation
2+
3+
/// Analytics 이벤트 타입
4+
public enum AnalyticsEvent {
5+
case app_open(deviceOS: String, appVersion: String)
6+
case view_splash
7+
case login_start(method: LoginMethod)
8+
case login_complete(method: LoginMethod)
9+
case interest_select(interests: [String])
10+
case onboarding_complete
11+
case view_home_pokit(entryPoint: String)
12+
case view_home_recommend(entryPoint: String)
13+
case add_folder(folderName: String)
14+
case add_link(folderId: String, linkDomain: String, entryPoint: String? = nil, linkId: String? = nil, positionIndex: Int? = nil, algoVersion: String? = nil)
15+
case view_folder_detail(folderId: String)
16+
case view_link_detail(linkId: String, linkDomain: String, entryPoint: String? = nil, positionIndex: Int? = nil, cardType: String? = nil, algoVersion: String? = nil)
17+
case share_link(linkId: String, shareTarget: String)
18+
case session_end(duration: Int)
19+
20+
/// 이벤트명 (rawValue)
21+
public var eventName: String {
22+
switch self {
23+
case .app_open: return "app_open"
24+
case .view_splash: return "view_splash"
25+
case .login_start: return "login_start"
26+
case .login_complete: return "login_complete"
27+
case .interest_select: return "interest_select"
28+
case .onboarding_complete: return "onboarding_complete"
29+
case .view_home_pokit: return "view_home_pokit"
30+
case .view_home_recommend: return "view_home_recommend"
31+
case .add_folder: return "add_folder"
32+
case .add_link: return "add_link"
33+
case .view_folder_detail: return "view_folder_detail"
34+
case .view_link_detail: return "view_link_detail"
35+
case .share_link: return "share_link"
36+
case .session_end: return "session_end"
37+
}
38+
}
39+
40+
/// Amplitude에 전송할 속성값
41+
public var properties: [String: Any] {
42+
switch self {
43+
case let .app_open(deviceOS, appVersion):
44+
return [
45+
PropertyKey.device_os.rawValue: deviceOS,
46+
PropertyKey.app_version.rawValue: appVersion
47+
]
48+
49+
case .view_splash:
50+
return [:]
51+
52+
case let .login_start(method):
53+
return [PropertyKey.method.rawValue: method.rawValue]
54+
55+
case let .login_complete(method):
56+
return [PropertyKey.method.rawValue: method.rawValue]
57+
58+
case let .interest_select(interests):
59+
return [PropertyKey.interests.rawValue: interests]
60+
61+
case .onboarding_complete:
62+
return [:]
63+
64+
case let .view_home_pokit(entryPoint):
65+
return [PropertyKey.entry_point.rawValue: entryPoint]
66+
67+
case let .view_home_recommend(entryPoint):
68+
return [PropertyKey.entry_point.rawValue: entryPoint]
69+
70+
case let .add_folder(folderName):
71+
return [PropertyKey.folder_name.rawValue: folderName]
72+
73+
case let .add_link(folderId, linkDomain, entryPoint, linkId, positionIndex, algoVersion):
74+
var props: [String: Any] = [
75+
PropertyKey.folder_id.rawValue: folderId,
76+
PropertyKey.link_domain.rawValue: linkDomain
77+
]
78+
if let entryPoint = entryPoint {
79+
props[PropertyKey.entry_point.rawValue] = entryPoint
80+
}
81+
if let linkId = linkId {
82+
props[PropertyKey.link_id.rawValue] = linkId
83+
}
84+
if let positionIndex = positionIndex {
85+
props[PropertyKey.position_index.rawValue] = positionIndex
86+
}
87+
if let algoVersion = algoVersion {
88+
props[PropertyKey.algo_version.rawValue] = algoVersion
89+
}
90+
return props
91+
92+
case let .view_folder_detail(folderId):
93+
return [PropertyKey.folder_id.rawValue: folderId]
94+
95+
case let .view_link_detail(linkId, linkDomain, entryPoint, positionIndex, cardType, algoVersion):
96+
var props: [String: Any] = [
97+
PropertyKey.link_id.rawValue: linkId,
98+
PropertyKey.link_domain.rawValue: linkDomain
99+
]
100+
if let entryPoint = entryPoint {
101+
props[PropertyKey.entry_point.rawValue] = entryPoint
102+
}
103+
if let positionIndex = positionIndex {
104+
props[PropertyKey.position_index.rawValue] = positionIndex
105+
}
106+
if let cardType = cardType {
107+
props[PropertyKey.card_type.rawValue] = cardType
108+
}
109+
if let algoVersion = algoVersion {
110+
props[PropertyKey.algo_version.rawValue] = algoVersion
111+
}
112+
return props
113+
114+
case let .share_link(linkId, shareTarget):
115+
return [
116+
PropertyKey.link_id.rawValue: linkId,
117+
PropertyKey.share_target.rawValue: shareTarget
118+
]
119+
120+
case let .session_end(duration):
121+
return [PropertyKey.duration.rawValue: duration]
122+
}
123+
}
124+
}
125+
126+
/// Analytics 속성 키
127+
public enum PropertyKey: String {
128+
case device_os
129+
case app_version
130+
case method
131+
case interests
132+
case entry_point
133+
case folder_name
134+
case folder_id
135+
case link_domain
136+
case link_id
137+
case share_target
138+
case duration
139+
case position_index
140+
case card_type
141+
case algo_version
142+
}
143+
144+
/// 로그인 방식
145+
public enum LoginMethod: String {
146+
case apple = "Apple"
147+
case google = "Google"
148+
case kakao = "Kakao"
149+
}
150+
151+
extension LoginMethod {
152+
init?(authPlatform: String) {
153+
switch authPlatform.lowercased() {
154+
case "apple":
155+
self = .apple
156+
case "google":
157+
self = .google
158+
case "kakao":
159+
self = .kakao
160+
default:
161+
return nil
162+
}
163+
}
164+
}

0 commit comments

Comments
 (0)