Skip to content

Commit cb17a2e

Browse files
authored
[FEAT/#1628] 앰플리튜드 이벤트 개편-1 (#1634)
* [FEAT/#1628] 솝레터 앰플리튜드 추가 * [FEAT/#1628] 메인 화면 탭 및 플러스 버튼 클릭 로깅 추가 - `MainTab`에 `Appjamtamp` 로깅 이름("navi_appjamtamp") 및 클릭 이벤트 타입 정의. - `MainScreen`에서 탭 선택 및 플러스(+) 버튼 클릭 시 `trackViewType`을 통한 분석 로그 전송 로직 추가. - `TrackerExt`에 이벤트 타입과 이름을 직접 전달받아 `view_type`을 트래킹하는 `trackViewType` 확장 함수 오버로딩 추가. * [MOD/#1628] Sopletter merge conflict 해결 - Move `CLICK_SOPTLETTER_START_BUTTON` analytics event from `SopletterOnboardingScreen` to `SopletterNameScreen`. - Remove unused `viewType` parameter from `SopletterEntryRoute` and its navigation call. - Update `sopletterGraph` and `SopletterNavHost` to accept and handle `userStatus`. * [CHORE/#1628] SopletterOnboardingScreen navigateToNickname 람다 제거
1 parent 2fa0224 commit cb17a2e

12 files changed

Lines changed: 258 additions & 20 deletions

File tree

core/analytics/src/main/java/org/sopt/official/analytics/TrackerExt.kt

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,45 @@ fun Tracker.track(event: AnalyticsEvent, properties: Map<String, Any?> = emptyMa
44
track(type = event.type, name = event.eventName, properties = properties)
55
}
66

7+
/**
8+
* view_type 프로퍼티를 포함해 이벤트를 전송하는 함수
9+
*
10+
* @param type 이벤트 타입
11+
* @param name 이벤트 이름
12+
* @param viewType 유저 상태 값
13+
* @param properties 추가 프로퍼티
14+
*/
715
fun Tracker.trackViewType(
8-
event: AnalyticsEvent,
16+
type: EventType,
17+
name: String,
918
viewType: String,
1019
properties: Map<String, Any?> = emptyMap(),
1120
) {
1221
track(
13-
event = event,
22+
type = type,
23+
name = name,
1424
properties = mapOf(
1525
AnalyticsPropertyKey.VIEW_TYPE to viewType,
1626
) + properties,
1727
)
1828
}
29+
30+
/**
31+
* view_type 프로퍼티를 포함해 이벤트를 전송하는 함수
32+
*
33+
* @param event 이벤트 객체
34+
* @param viewType 유저 상태 값
35+
* @param properties 추가 프로퍼티
36+
*/
37+
fun Tracker.trackViewType(
38+
event: AnalyticsEvent,
39+
viewType: String,
40+
properties: Map<String, Any?> = emptyMap(),
41+
) {
42+
trackViewType(
43+
type = event.type,
44+
name = event.eventName,
45+
viewType = viewType,
46+
properties = properties,
47+
)
48+
}

feature/main/src/main/java/org/sopt/official/feature/main/MainScreen.kt

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,8 @@ import androidx.navigation.compose.NavHost
8383
import kotlinx.collections.immutable.ImmutableList
8484
import kotlinx.collections.immutable.toImmutableList
8585
import kotlinx.collections.immutable.toPersistentList
86-
import org.sopt.official.analytics.EventType
8786
import org.sopt.official.analytics.compose.LocalTracker
87+
import org.sopt.official.analytics.trackViewType
8888
import org.sopt.official.common.navigator.DeepLinkType
8989
import org.sopt.official.common.navigator.NavigatorProvider
9090
import org.sopt.official.common.view.toast
@@ -408,6 +408,7 @@ fun MainScreen(
408408
}
409409

410410
sopletterGraph(
411+
userStatus = userStatus,
411412
navigateToHome = {
412413
if (!navigator.navController.popBackStack()) {
413414
navigator.navigateAndClear(MainTab.Home, userStatus)
@@ -429,11 +430,11 @@ fun MainScreen(
429430
showBadgeContent = badgeList,
430431
currentTab = navigator.currentTab,
431432
onTabSelected = { selectedTab ->
432-
if (selectedTab.loggingName != null) {
433-
tracker.track(
434-
name = selectedTab.loggingName,
435-
type = EventType.CLICK,
436-
properties = mapOf("view_type" to userStatus.value)
433+
selectedTab.loggingName?.let { loggingName ->
434+
tracker.trackViewType(
435+
type = MainTab.CLICK_EVENT_TYPE,
436+
name = loggingName,
437+
viewType = userStatus.value,
437438
)
438439
}
439440

@@ -448,7 +449,14 @@ fun MainScreen(
448449
}
449450
},
450451
isMenuOpen = isFloatingMenuOpen,
451-
onMenuToggle = { isFloatingMenuOpen = !isFloatingMenuOpen }
452+
onMenuToggle = {
453+
tracker.trackViewType(
454+
type = MainTab.CLICK_EVENT_TYPE,
455+
name = MainTab.PLUS_BUTTON_LOGGING_NAME,
456+
viewType = userStatus.value,
457+
)
458+
isFloatingMenuOpen = !isFloatingMenuOpen
459+
}
452460
)
453461
}
454462
}

feature/main/src/main/java/org/sopt/official/feature/main/MainTab.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@ package org.sopt.official.feature.main
2626

2727
import androidx.annotation.DrawableRes
2828
import androidx.compose.runtime.Composable
29+
import org.sopt.official.analytics.EventType
2930
import org.sopt.official.core.navigation.MainTabRoute
3031
import org.sopt.official.core.navigation.Route
32+
import org.sopt.official.feature.appjamtamp.navigation.AppjamtampNavGraph
3133
import org.sopt.official.feature.mypage.navigation.MyPageGraph
3234

3335
enum class MainTab(
@@ -56,8 +58,8 @@ enum class MainTab(
5658
Appjamtamp(
5759
icon = R.drawable.ic_main_soptamp,
5860
contentDescription = "솝탬프",
59-
route = org.sopt.official.feature.appjamtamp.navigation.AppjamtampNavGraph,
60-
loggingName = null,
61+
route = AppjamtampNavGraph,
62+
loggingName = "navi_appjamtamp",
6163
deeplink = "appjamtamp"
6264
),
6365

@@ -78,6 +80,9 @@ enum class MainTab(
7880
);
7981

8082
companion object {
83+
val CLICK_EVENT_TYPE = EventType.CLICK
84+
const val PLUS_BUTTON_LOGGING_NAME = "plus_button"
85+
8186
@Composable
8287
fun find(predicate: @Composable (MainTabRoute) -> Boolean): MainTab? {
8388
return entries.find { predicate(it.route) }

feature/sopletter/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ android {
3333

3434
dependencies {
3535
// core
36+
implementation(projects.core.analytics)
3637
implementation(projects.core.common)
3738
implementation(projects.core.designsystem)
3839
implementation(projects.core.model)
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package org.sopt.official.feature.sopletter
2+
3+
import org.sopt.official.analytics.AnalyticsEvent
4+
import org.sopt.official.analytics.EventType
5+
6+
enum class SopletterAnalyticsEvent(
7+
override val type: EventType,
8+
override val eventName: String,
9+
) : AnalyticsEvent {
10+
VIEW_SOPTLETTER_ONBOARDING(
11+
type = EventType.VIEW,
12+
eventName = "soptletter_onboarding",
13+
),
14+
VIEW_SOPTLETTER_NICKNAME(
15+
type = EventType.VIEW,
16+
eventName = "soptletter_nickname",
17+
),
18+
VIEW_SOPTLETTER_MAIN(
19+
type = EventType.VIEW,
20+
eventName = "soptletter_main",
21+
),
22+
CLICK_SOPTLETTER_START_BUTTON(
23+
type = EventType.CLICK,
24+
eventName = "soptletter_start_button",
25+
),
26+
CLICK_WRITE_SOPTLETTER(
27+
type = EventType.CLICK,
28+
eventName = "write_soptletter",
29+
),
30+
CLICK_DONE_WRITE_SOPTLETTER(
31+
type = EventType.CLICK,
32+
eventName = "done_write_soptletter",
33+
),
34+
CLICK_SOPTLETTER_DETAIL(
35+
type = EventType.CLICK,
36+
eventName = "soptletter_detail",
37+
),
38+
CLICK_EDIT_SOPTLETTER(
39+
type = EventType.CLICK,
40+
eventName = "edit_soptletter",
41+
),
42+
CLICK_DONE_EDIT_SOPTLETTER(
43+
type = EventType.CLICK,
44+
eventName = "done_edit_soptletter",
45+
),
46+
CLICK_SOPTLETTER_LIKE_BUTTON(
47+
type = EventType.CLICK,
48+
eventName = "soptletter_like_button",
49+
),
50+
CLICK_DELETE_SOPTLETTER(
51+
type = EventType.CLICK,
52+
eventName = "delete_soptletter",
53+
),
54+
CLICK_EXPORT_SOPTLETTER(
55+
type = EventType.CLICK,
56+
eventName = "export_soptletter",
57+
),
58+
CLICK_DONE_EXPORT_SOPTLETTER(
59+
type = EventType.CLICK,
60+
eventName = "done_export_soptletter",
61+
),
62+
CLICK_QUIT_SOPTLETTER(
63+
type = EventType.CLICK,
64+
eventName = "quit_soptletter",
65+
),
66+
}

feature/sopletter/src/main/java/org/sopt/official/feature/sopletter/main/SopletterMainScreen.kt

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,18 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
6060
import androidx.lifecycle.compose.LocalLifecycleOwner
6161
import androidx.lifecycle.compose.collectAsStateWithLifecycle
6262
import androidx.lifecycle.flowWithLifecycle
63+
import org.sopt.official.analytics.compose.LocalTracker
64+
import org.sopt.official.analytics.trackViewType
6365
import org.sopt.official.common.util.onBottomReached
6466
import org.sopt.official.designsystem.SoptTheme
6567
import org.sopt.official.designsystem.component.dialog.NetworkErrorDialog
6668
import org.sopt.official.domain.sopletter.model.SopletterMessage
69+
import org.sopt.official.feature.sopletter.SopletterAnalyticsEvent
70+
import org.sopt.official.feature.sopletter.common.component.SopletterScaffold
6771
import org.sopt.official.feature.sopletter.common.component.SopletterPrintDialog
6872
import org.sopt.official.feature.sopletter.common.model.SopletterSnackbarVisuals
73+
import org.sopt.official.feature.sopletter.common.component.SopletterSnackbarHost
74+
import org.sopt.official.feature.sopletter.main.analytics.TrackedSopletterMemoDetailDialogActions
6975
import org.sopt.official.feature.sopletter.main.component.EmptySopletterContent
7076
import org.sopt.official.feature.sopletter.main.component.SopletterDeleteDialog
7177
import org.sopt.official.feature.sopletter.main.component.SopletterMainTopBar
@@ -83,6 +89,7 @@ import org.sopt.official.webview.view.WebViewActivity
8389
@OptIn(ExperimentalMaterialApi::class)
8490
@Composable
8591
fun SopletterMainRoute(
92+
viewType: String,
8693
onShowSnackbar: (SopletterSnackbarVisuals) -> Unit,
8794
viewModel: SopletterMainViewModel = hiltViewModel(),
8895
navigateUp: () -> Unit = {},
@@ -95,9 +102,16 @@ fun SopletterMainRoute(
95102
val dialogActions: SopletterMemoDetailDialogContract.Actions = viewModel
96103
val context = LocalContext.current
97104
val lifecycleOwner = LocalLifecycleOwner.current
105+
val tracker = LocalTracker.current
106+
val trackedDialogActions = remember(dialogActions, tracker, viewType) {
107+
TrackedSopletterMemoDetailDialogActions(dialogActions) { event ->
108+
tracker.trackViewType(event, viewType)
109+
}
110+
}
98111
var isPrintDialogVisible by remember { mutableStateOf(false) }
99112

100113
LaunchedEffect(Unit) {
114+
tracker.trackViewType(SopletterAnalyticsEvent.VIEW_SOPTLETTER_MAIN, viewType)
101115
viewModel.sideEffect.flowWithLifecycle(lifecycleOwner.lifecycle)
102116
.collect { sideEffect ->
103117
when (sideEffect) {
@@ -118,16 +132,25 @@ fun SopletterMainRoute(
118132
SopletterMainScreen(
119133
uiState = uiState,
120134
onMemoClick = {
135+
tracker.trackViewType(SopletterAnalyticsEvent.CLICK_SOPTLETTER_DETAIL, viewType)
121136
viewModel.fetchMemoDetail(
122137
messageId = it.messageId,
123138
memoColor = it.memoColor(),
124139
)
125140
},
126141
onRefresh = viewModel::refreshMainContent,
127142
onLoadMore = { viewModel.fetchMessages(isLoadMore = true) },
128-
dialogActions = dialogActions,
129-
onBackClick = navigateUp,
130-
onDownloadClick = { isPrintDialogVisible = true },
143+
dialogActions = trackedDialogActions,
144+
onBackClick = {
145+
if (uiState.routeTopicId == null) {
146+
tracker.trackViewType(SopletterAnalyticsEvent.CLICK_QUIT_SOPTLETTER, viewType)
147+
}
148+
navigateUp()
149+
},
150+
onDownloadClick = {
151+
tracker.trackViewType(SopletterAnalyticsEvent.CLICK_EXPORT_SOPTLETTER, viewType)
152+
isPrintDialogVisible = true
153+
},
131154
isPrintDialogVisible = isPrintDialogVisible,
132155
onPrintDialogDismiss = { isPrintDialogVisible = false },
133156
onPrintConfirm = {
@@ -139,7 +162,10 @@ fun SopletterMainRoute(
139162
onTopicCtaClick = navigateToTopicDetail,
140163
onReportClick = viewModel::openReportForm,
141164
onErrorConfirm = viewModel::dismissErrorDialog,
142-
onWriteFABClick = { navigateToWrite(uiState.selectedTopicId) },
165+
onWriteFABClick = {
166+
tracker.trackViewType(SopletterAnalyticsEvent.CLICK_WRITE_SOPTLETTER, viewType)
167+
navigateToWrite(uiState.selectedTopicId)
168+
},
143169
)
144170
}
145171

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* MIT License
3+
* Copyright 2026 SOPT - Shout Our Passion Together
4+
*
5+
* Permission is hereby granted, free of charge, to any person obtaining a copy
6+
* of this software and associated documentation files (the "Software"), to deal
7+
* in the Software without restriction, including without limitation the rights
8+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
* copies of the Software, and to permit persons to whom the Software is
10+
* furnished to do so, subject to the following conditions:
11+
*
12+
* The above copyright notice and this permission notice shall be included in all
13+
* copies or substantial portions of the Software.
14+
*
15+
* https://www.apache.org/licenses/LICENSE-2.0
16+
*
17+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23+
* SOFTWARE.
24+
*/
25+
package org.sopt.official.feature.sopletter.main.analytics
26+
27+
import org.sopt.official.feature.sopletter.SopletterAnalyticsEvent
28+
import org.sopt.official.feature.sopletter.main.contract.SopletterMemoDetailDialogContract
29+
30+
internal class TrackedSopletterMemoDetailDialogActions(
31+
private val delegate: SopletterMemoDetailDialogContract.Actions,
32+
private val trackEvent: (SopletterAnalyticsEvent) -> Unit,
33+
) : SopletterMemoDetailDialogContract.Actions by delegate {
34+
override fun onLikeClick() {
35+
trackEvent(SopletterAnalyticsEvent.CLICK_SOPTLETTER_LIKE_BUTTON)
36+
delegate.onLikeClick()
37+
}
38+
39+
override fun onEditClick() {
40+
trackEvent(SopletterAnalyticsEvent.CLICK_EDIT_SOPTLETTER)
41+
delegate.onEditClick()
42+
}
43+
44+
override fun onEditCompleteClick(content: String) {
45+
trackEvent(SopletterAnalyticsEvent.CLICK_DONE_EDIT_SOPTLETTER)
46+
delegate.onEditCompleteClick(content)
47+
}
48+
49+
override fun onDeleteConfirmClick() {
50+
trackEvent(SopletterAnalyticsEvent.CLICK_DELETE_SOPTLETTER)
51+
delegate.onDeleteConfirmClick()
52+
}
53+
}

feature/sopletter/src/main/java/org/sopt/official/feature/sopletter/name/SopletterNameScreen.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,22 +41,28 @@ import androidx.lifecycle.Lifecycle
4141
import androidx.lifecycle.compose.LocalLifecycleOwner
4242
import androidx.lifecycle.compose.collectAsStateWithLifecycle
4343
import androidx.lifecycle.repeatOnLifecycle
44+
import org.sopt.official.analytics.compose.LocalTracker
45+
import org.sopt.official.analytics.trackViewType
4446
import org.sopt.official.designsystem.SoptTheme
47+
import org.sopt.official.feature.sopletter.SopletterAnalyticsEvent
4548
import org.sopt.official.feature.sopletter.common.component.SopletterButton
4649
import org.sopt.official.feature.sopletter.common.component.SopletterTopbar
4750
import org.sopt.official.feature.sopletter.name.component.SopletterNameInfoHolder
4851
import org.sopt.official.sopletter.R
4952

5053
@Composable
5154
fun SopletterNameRoute(
55+
viewType: String,
5256
navigateToSopletterMain: () -> Unit,
5357
navigateToHome: () -> Unit,
5458
viewModel: SopletterNameViewModel = hiltViewModel()
5559
) {
5660
val lifeCycleOwner = LocalLifecycleOwner.current
5761
val state by viewModel.state.collectAsStateWithLifecycle()
62+
val tracker = LocalTracker.current
5863

5964
LaunchedEffect(Unit) {
65+
tracker.trackViewType(SopletterAnalyticsEvent.VIEW_SOPTLETTER_NICKNAME, viewType)
6066
lifeCycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
6167
viewModel.sideEffect.collect { sideEffect ->
6268
when (sideEffect) {
@@ -68,7 +74,10 @@ fun SopletterNameRoute(
6874

6975
SopletterNameScreen(
7076
state = state,
71-
navigateToSopletterMain = navigateToSopletterMain,
77+
navigateToSopletterMain = {
78+
tracker.trackViewType(SopletterAnalyticsEvent.CLICK_SOPTLETTER_START_BUTTON, viewType)
79+
navigateToSopletterMain()
80+
},
7281
navigateToHome = navigateToHome,
7382
)
7483
}

0 commit comments

Comments
 (0)