From e8bff17011fd63354f395f8e0a043a30cb6b63a8 Mon Sep 17 00:00:00 2001 From: Hyobeen-Park Date: Sat, 5 Jul 2025 19:05:06 +0900 Subject: [PATCH 01/31] =?UTF-8?q?[ADD/#386]=20=EB=AF=B9=EC=8A=A4=ED=8C=A8?= =?UTF-8?q?=EB=84=90=20=EC=9D=98=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 --- app/build.gradle.kts | 14 ++++++++++++++ gradle/libs.versions.toml | 6 ++++++ 2 files changed, 20 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 014c5f1a9..574eaaf3e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -54,6 +54,12 @@ android { "BASE_URL", properties.getProperty("dev.base.url") ) + + buildConfigField( + "String", + "MIXPANEL_KEY", + properties["mixpanelDevKey"] as? String ?: "" + ) } release { @@ -63,6 +69,12 @@ android { properties.getProperty("prod.base.url") ) + buildConfigField( + "String", + "MIXPANEL_KEY", + properties["mixpanelProdKey"] as? String ?: "" + ) + isMinifyEnabled = true isShrinkResources = true proguardFiles( @@ -135,6 +147,8 @@ dependencies { implementation(libs.accompanist.systemuicontroller) implementation(libs.play.services.oss.licenses) + + implementation(libs.mixpanel) } ktlint { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b55355454..56c017b88 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -72,6 +72,9 @@ playServicesMaps = "19.0.0" ## Room room = "2.6.1" +## mixpanel +mixpanel = "7.+" + [libraries] # Test junit = { group = "junit", name = "junit", version.ref = "junit" } @@ -150,6 +153,9 @@ room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } +## Mixpanel +mixpanel = { group = "com.mixpanel.android", name = "mixpanel-android", version.ref = "mixpanel"} + [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } From 9bd5d7497233aba3684a68eefec8834d92b4e236 Mon Sep 17 00:00:00 2001 From: Hyobeen-Park Date: Sat, 5 Jul 2025 19:05:49 +0900 Subject: [PATCH 02/31] =?UTF-8?q?[FEAT/#386]=20=EB=AF=B9=EC=8A=A4=ED=8C=A8?= =?UTF-8?q?=EB=84=90=20=ED=8A=B8=EB=9E=98=EC=BB=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spoony/core/analytics/MixPanelTracker.kt | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt b/app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt new file mode 100644 index 000000000..c2d901e1a --- /dev/null +++ b/app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt @@ -0,0 +1,39 @@ +package com.spoony.spoony.core.analytics + +import android.content.Context +import androidx.compose.runtime.staticCompositionLocalOf +import com.mixpanel.android.mpmetrics.MixpanelAPI +import com.spoony.spoony.BuildConfig.MIXPANEL_KEY +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import org.json.JSONObject + +val LocalTracker = staticCompositionLocalOf { + error("No MixpanelTracker provided") +} + +class MixPanelTracker @Inject constructor( + @ApplicationContext private val context: Context +) { + private val mixpanel = MixpanelAPI.getInstance( + context, + MIXPANEL_KEY, + false + ) + + fun track(eventName: String) { + mixpanel.track(eventName) + } + + fun track(eventName: String, properties: String) { + mixpanel.track(eventName, properties.toJsonObject()) + } + + private fun String.toJsonObject(): JSONObject { + return try { + JSONObject(this) + } catch (e: Exception) { + JSONObject() + } + } +} From 0a22c869ab037feef475aede94f14b4b0cb106f7 Mon Sep 17 00:00:00 2001 From: Hyobeen-Park Date: Sat, 5 Jul 2025 19:11:27 +0900 Subject: [PATCH 03/31] =?UTF-8?q?[FEAT/#386]=20=EB=A1=9C=EC=BB=AC=20?= =?UTF-8?q?=ED=8A=B8=EB=9E=98=EC=BB=A4=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/spoony/spoony/presentation/MainActivity.kt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/spoony/spoony/presentation/MainActivity.kt b/app/src/main/java/com/spoony/spoony/presentation/MainActivity.kt index 7b30fb37e..2d5f4109e 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/MainActivity.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/MainActivity.kt @@ -6,12 +6,19 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.compose.runtime.CompositionLocalProvider +import com.spoony.spoony.core.analytics.LocalTracker +import com.spoony.spoony.core.analytics.MixPanelTracker import com.spoony.spoony.core.designsystem.theme.SpoonyAndroidTheme import com.spoony.spoony.presentation.main.MainScreen import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject @AndroidEntryPoint class MainActivity : ComponentActivity() { + @Inject + lateinit var tracker: MixPanelTracker + @SuppressLint("SourceLockedOrientationActivity") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -19,7 +26,9 @@ class MainActivity : ComponentActivity() { enableEdgeToEdge() setContent { SpoonyAndroidTheme { - MainScreen() + CompositionLocalProvider(LocalTracker provides tracker) { + MainScreen() + } } } } From c9adc9a51fe9808602740f69c8f235a440b7a612 Mon Sep 17 00:00:00 2001 From: Hyobeen-Park Date: Sat, 5 Jul 2025 19:26:23 +0900 Subject: [PATCH 04/31] =?UTF-8?q?[FEAT/#386]=20=EC=A0=84=ED=99=98=20?= =?UTF-8?q?=EB=B6=84=EC=84=9D=EC=9A=A9=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=8B=AC=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/spoony/spoony/core/analytics/MixPanelTracker.kt | 3 +++ .../presentation/auth/onboarding/OnboardingScreen.kt | 6 ++++++ .../spoony/presentation/auth/signin/SignInScreen.kt | 9 ++++++++- .../spoony/spoony/presentation/splash/SplashScreen.kt | 4 ++++ 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt b/app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt index c2d901e1a..9124b30ea 100644 --- a/app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt +++ b/app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt @@ -7,6 +7,7 @@ import com.spoony.spoony.BuildConfig.MIXPANEL_KEY import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import org.json.JSONObject +import timber.log.Timber val LocalTracker = staticCompositionLocalOf { error("No MixpanelTracker provided") @@ -22,10 +23,12 @@ class MixPanelTracker @Inject constructor( ) fun track(eventName: String) { + Timber.tag("mixpanel").d(eventName) mixpanel.track(eventName) } fun track(eventName: String, properties: String) { + Timber.tag("mixpanel").d("$mixpanel $properties") mixpanel.track(eventName, properties.toJsonObject()) } diff --git a/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingScreen.kt index e8a8329ff..767954cb4 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingScreen.kt @@ -17,6 +17,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController import androidx.navigation.navOptions +import com.spoony.spoony.core.analytics.LocalTracker import com.spoony.spoony.core.designsystem.component.topappbar.SpoonyBasicTopAppBar import com.spoony.spoony.core.designsystem.event.LocalSnackBarTrigger import com.spoony.spoony.core.designsystem.theme.SpoonyAndroidTheme @@ -48,9 +49,14 @@ private fun OnboardingScreen( val state by viewModel.state.collectAsStateWithLifecycle() val showSnackbar = LocalSnackBarTrigger.current + val tracker = LocalTracker.current + when (state.signUpState) { is UiState.Empty -> { viewModel.updateCurrentStep(OnboardingSteps.END) + + tracker.track("signup_completed", "\"signup_method\" : \"kakao\"") + navController.navigate( route = End, navOptions = navOptions { diff --git a/app/src/main/java/com/spoony/spoony/presentation/auth/signin/SignInScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/auth/signin/SignInScreen.kt index 4c4b91a28..02581dbdc 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/auth/signin/SignInScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/auth/signin/SignInScreen.kt @@ -25,6 +25,7 @@ import androidx.lifecycle.flowWithLifecycle import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.kakao.sdk.user.UserApiClient import com.spoony.spoony.R +import com.spoony.spoony.core.analytics.LocalTracker import com.spoony.spoony.core.designsystem.event.LocalSnackBarTrigger import com.spoony.spoony.core.designsystem.theme.SpoonyAndroidTheme import com.spoony.spoony.core.designsystem.theme.main100 @@ -42,6 +43,8 @@ fun SignInRoute( val systemUiController = rememberSystemUiController() val showSnackbar = LocalSnackBarTrigger.current + val tracker = LocalTracker.current + LaunchedEffect(Unit) { systemUiController.setNavigationBarColor( color = main100 @@ -54,7 +57,11 @@ fun SignInRoute( when (sideEffect) { is SignInSideEffect.ShowSnackBar -> showSnackbar(sideEffect.message) is SignInSideEffect.NavigateToSignUp -> navigateToTermsOfService() - is SignInSideEffect.NavigateToMap -> navigateToMap() + is SignInSideEffect.NavigateToMap -> { + tracker.track("login_success") + + navigateToMap() + } is SignInSideEffect.StartKakaoTalkLogin -> { UserApiClient.instance.loginWithKakaoTalk( context = context, diff --git a/app/src/main/java/com/spoony/spoony/presentation/splash/SplashScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/splash/SplashScreen.kt index c75c6093e..85dc506bf 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/splash/SplashScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/splash/SplashScreen.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.spoony.spoony.R +import com.spoony.spoony.core.analytics.LocalTracker import com.spoony.spoony.core.designsystem.theme.SpoonyAndroidTheme import com.spoony.spoony.core.designsystem.theme.main400 @@ -28,12 +29,15 @@ fun SplashRoute( viewModel: SplashViewModel = hiltViewModel() ) { val systemUiController = rememberSystemUiController() + val tracker = LocalTracker.current LaunchedEffect(Unit) { systemUiController.setNavigationBarColor( color = main400 ) + tracker.track("app_open") + if (viewModel.hasAccessToken()) { navigateToMap() } else { From c2cf3c8b0b7704800a0ae1ec4c7995e118d0a17b Mon Sep 17 00:00:00 2001 From: Hyobeen-Park Date: Sat, 5 Jul 2025 23:25:43 +0900 Subject: [PATCH 05/31] =?UTF-8?q?[CHORE/#386]=20=EC=8B=A4=EC=88=98=20?= =?UTF-8?q?=EB=B0=94=EB=A1=9C=EC=9E=A1=EA=B8=B0...?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/spoony/spoony/core/analytics/MixPanelTracker.kt | 2 +- .../presentation/auth/onboarding/OnboardingEndScreen.kt | 4 ++++ .../spoony/presentation/auth/onboarding/OnboardingScreen.kt | 4 ---- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt b/app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt index 9124b30ea..6614c650b 100644 --- a/app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt +++ b/app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt @@ -28,7 +28,7 @@ class MixPanelTracker @Inject constructor( } fun track(eventName: String, properties: String) { - Timber.tag("mixpanel").d("$mixpanel $properties") + Timber.tag("mixpanel").d("$eventName $properties") mixpanel.track(eventName, properties.toJsonObject()) } diff --git a/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingEndScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingEndScreen.kt index 4c083e5c1..f5c0c0c13 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingEndScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingEndScreen.kt @@ -23,6 +23,7 @@ import com.airbnb.lottie.compose.LottieCompositionSpec import com.airbnb.lottie.compose.LottieConstants import com.airbnb.lottie.compose.rememberLottieComposition import com.spoony.spoony.R +import com.spoony.spoony.core.analytics.LocalTracker import com.spoony.spoony.core.designsystem.component.button.SpoonyButton import com.spoony.spoony.core.designsystem.theme.SpoonyAndroidTheme import com.spoony.spoony.core.designsystem.type.ButtonSize @@ -35,8 +36,11 @@ fun OnboardingEndRoute( ) { val state by viewModel.state.collectAsStateWithLifecycle() + val tracker = LocalTracker.current + LaunchedEffect(Unit) { viewModel.updateCurrentStep(OnboardingSteps.END) + tracker.track("signup_completed", "{\"signup_method\" : \"kakao\"}") } OnboardingEndScreen( diff --git a/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingScreen.kt index 767954cb4..de55a8d17 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingScreen.kt @@ -49,14 +49,10 @@ private fun OnboardingScreen( val state by viewModel.state.collectAsStateWithLifecycle() val showSnackbar = LocalSnackBarTrigger.current - val tracker = LocalTracker.current - when (state.signUpState) { is UiState.Empty -> { viewModel.updateCurrentStep(OnboardingSteps.END) - tracker.track("signup_completed", "\"signup_method\" : \"kakao\"") - navController.navigate( route = End, navOptions = navOptions { From 320c149271ecad16145993063df9905d7eb95feb Mon Sep 17 00:00:00 2001 From: Hyobeen-Park Date: Tue, 8 Jul 2025 14:08:12 +0900 Subject: [PATCH 06/31] =?UTF-8?q?[FEAT/#386]=20=ED=83=AD=20=EC=A7=84?= =?UTF-8?q?=EC=9E=85=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spoony/presentation/explore/ExploreScreen.kt | 11 +++++++++++ .../spoony/presentation/gourmet/map/MapScreen.kt | 3 +++ .../spoony/presentation/register/RegisterScreen.kt | 6 ++++++ .../presentation/userpage/mypage/MyPageRoute.kt | 6 ++++++ 4 files changed, 26 insertions(+) diff --git a/app/src/main/java/com/spoony/spoony/presentation/explore/ExploreScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/explore/ExploreScreen.kt index b475e4cca..228082bf9 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/explore/ExploreScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/explore/ExploreScreen.kt @@ -45,6 +45,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.repeatOnLifecycle import com.spoony.spoony.R +import com.spoony.spoony.core.analytics.LocalTracker import com.spoony.spoony.core.designsystem.component.card.ReviewCard import com.spoony.spoony.core.designsystem.component.dialog.TwoButtonDialog import com.spoony.spoony.core.designsystem.component.pullToRefresh.SpoonyPullToRefreshContainer @@ -89,8 +90,14 @@ fun ExploreRoute( val lifecycleOwner = LocalLifecycleOwner.current val showSnackBar = LocalSnackBarTrigger.current + val tracker = LocalTracker.current val coroutineScope = rememberCoroutineScope() val listState = rememberLazyListState() + + LaunchedEffect(Unit) { + tracker.track("review_viewed", "{\"tab_name\" : \"explore\"}") + } + LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { viewModel.sideEffect.flowWithLifecycle(lifecycleOwner.lifecycle).collect { effect -> when (effect) { @@ -113,6 +120,10 @@ fun ExploreRoute( } } + LaunchedEffect(Unit) { + tracker.track("tab_entered", "{\"tab_name\" : \"explore\"}") + } + with(state) { ExploreScreen( paddingValues = paddingValues, diff --git a/app/src/main/java/com/spoony/spoony/presentation/gourmet/map/MapScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/gourmet/map/MapScreen.kt index 8c6aefaed..cf8fa7dd2 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/gourmet/map/MapScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/gourmet/map/MapScreen.kt @@ -78,6 +78,7 @@ import com.naver.maps.map.compose.NaverMap import com.naver.maps.map.compose.rememberCameraPositionState import com.naver.maps.map.location.FusedLocationSource import com.spoony.spoony.R +import com.spoony.spoony.core.analytics.LocalTracker import com.spoony.spoony.core.designsystem.component.bottomsheet.SpoonyAdvancedBottomSheet import com.spoony.spoony.core.designsystem.component.bottomsheet.SpoonyBasicDragHandle import com.spoony.spoony.core.designsystem.component.chip.IconChip @@ -132,6 +133,7 @@ fun MapRoute( val context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current val showSnackBar = LocalSnackBarTrigger.current + val tracker = LocalTracker.current val cameraPositionState = rememberCameraPositionState { position = CameraPosition( @@ -193,6 +195,7 @@ fun MapRoute( } LaunchedEffect(Unit) { + tracker.track("tab_entered", "{\"tab_name\" : \"map\"}") when { state.locationModel.placeId != null -> { moveCamera( diff --git a/app/src/main/java/com/spoony/spoony/presentation/register/RegisterScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/register/RegisterScreen.kt index 56ddc6977..b7760e79d 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/register/RegisterScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/register/RegisterScreen.kt @@ -23,12 +23,14 @@ import androidx.lifecycle.flowWithLifecycle import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController +import com.spoony.spoony.core.analytics.LocalTracker import com.spoony.spoony.core.designsystem.component.topappbar.TitleTopAppBar import com.spoony.spoony.core.designsystem.event.LocalSnackBarTrigger import com.spoony.spoony.core.designsystem.theme.SpoonyAndroidTheme import com.spoony.spoony.core.util.extension.noRippleClickable import com.spoony.spoony.presentation.register.component.TopLinearProgressBar import com.spoony.spoony.presentation.register.model.RegisterState +import com.spoony.spoony.presentation.register.model.RegisterType import com.spoony.spoony.presentation.register.navigation.RegisterRoute import com.spoony.spoony.presentation.register.navigation.registerGraph @@ -45,6 +47,7 @@ fun RegisterRoute( val navController = rememberNavController() val showSnackBar = LocalSnackBarTrigger.current val lifecycleOwner = LocalLifecycleOwner.current + val tracker = LocalTracker.current LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { viewModel.sideEffect.flowWithLifecycle(lifecycleOwner.lifecycle).collect { effect -> @@ -62,6 +65,9 @@ fun RegisterRoute( LaunchedEffect(Unit) { viewModel.loadState() + if(viewModel.registerType == RegisterType.CREATE) { + tracker.track("tab_entered", "{\"tab_name\" : \"upload\"}") + } } RegisterScreen( diff --git a/app/src/main/java/com/spoony/spoony/presentation/userpage/mypage/MyPageRoute.kt b/app/src/main/java/com/spoony/spoony/presentation/userpage/mypage/MyPageRoute.kt index 5cb6d2c80..cef7d77e3 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/userpage/mypage/MyPageRoute.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/userpage/mypage/MyPageRoute.kt @@ -10,6 +10,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle +import com.spoony.spoony.core.analytics.LocalTracker import com.spoony.spoony.core.designsystem.event.LocalSnackBarTrigger import com.spoony.spoony.core.designsystem.theme.SpoonyAndroidTheme import com.spoony.spoony.presentation.follow.model.FollowType @@ -35,10 +36,15 @@ fun MyPageRoute( val userPageState by viewModel.state.collectAsStateWithLifecycle() val showSnackBar = LocalSnackBarTrigger.current val lifecycleOwner = LocalLifecycleOwner.current + val tracker = LocalTracker.current LaunchedEffect(Unit) { viewModel.getUserProfile() viewModel.getSpoonCount() + + if(userPageState.userType == UserType.MY_PAGE) { + tracker.track("tab_entered", "{\"tab_name\" : \"mypage\"}") + } } LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { From efcb99fc1248cad57a78522a3c0b56fc18f8e601 Mon Sep 17 00:00:00 2001 From: Hyobeen-Park Date: Fri, 3 Oct 2025 03:25:31 +0900 Subject: [PATCH 07/31] =?UTF-8?q?[FEAT/#386]=20=EC=83=81=EC=84=B8=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EC=A7=84=EC=9E=85=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spoony/core/analytics/MixPanelTracker.kt | 1 + .../auth/onboarding/OnboardingScreen.kt | 1 - .../placeDetail/PlaceDetailRoute.kt | 25 ++++++++++++++++++- .../presentation/register/RegisterScreen.kt | 2 +- .../userpage/mypage/MyPageRoute.kt | 2 +- 5 files changed, 27 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt b/app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt index 6614c650b..7d78c750c 100644 --- a/app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt +++ b/app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt @@ -36,6 +36,7 @@ class MixPanelTracker @Inject constructor( return try { JSONObject(this) } catch (e: Exception) { + Timber.e(e) JSONObject() } } diff --git a/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingScreen.kt index de55a8d17..39b63797f 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingScreen.kt @@ -17,7 +17,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController import androidx.navigation.navOptions -import com.spoony.spoony.core.analytics.LocalTracker import com.spoony.spoony.core.designsystem.component.topappbar.SpoonyBasicTopAppBar import com.spoony.spoony.core.designsystem.event.LocalSnackBarTrigger import com.spoony.spoony.core.designsystem.theme.SpoonyAndroidTheme diff --git a/app/src/main/java/com/spoony/spoony/presentation/placeDetail/PlaceDetailRoute.kt b/app/src/main/java/com/spoony/spoony/presentation/placeDetail/PlaceDetailRoute.kt index c3aa7dd8d..21ff19924 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/placeDetail/PlaceDetailRoute.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/placeDetail/PlaceDetailRoute.kt @@ -38,6 +38,7 @@ import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.repeatOnLifecycle +import com.spoony.spoony.core.analytics.LocalTracker import com.spoony.spoony.core.designsystem.component.button.FollowButton import com.spoony.spoony.core.designsystem.component.snackbar.TextSnackbar import com.spoony.spoony.core.designsystem.component.topappbar.TagTopAppBar @@ -76,6 +77,7 @@ fun PlaceDetailRoute( viewModel: PlaceDetailViewModel = hiltViewModel() ) { val lifecycleOwner = LocalLifecycleOwner.current + val tracker = LocalTracker.current val state by viewModel.state.collectAsStateWithLifecycle(lifecycleOwner = lifecycleOwner) @@ -101,6 +103,7 @@ fun PlaceDetailRoute( is PlaceDetailSideEffect.ShowSnackbar -> { onShowSnackBar(effect.message) } + is PlaceDetailSideEffect.NavigateUp -> navigateUp() } } @@ -130,12 +133,29 @@ fun PlaceDetailRoute( ) } - when (state.placeDetailModel) { + when (val uiState = state.placeDetailModel) { is UiState.Empty -> {} is UiState.Loading -> {} is UiState.Failure -> {} is UiState.Success -> { val postId = (state.reviewId as? UiState.Success)?.data ?: return + + tracker.track( + eventName = "review_viewed", + properties = "{\"review_id\" : \"$postId\"," + + "\"author_user_id\" : \"${userProfile.userId}\"," + + "\"place_name\" : \"${uiState.data.placeName}\"," + + "\"menu_count\" : ${uiState.data.menuList.size}," + + "\"satisfaction_score\" : ${uiState.data.value}," + + "\"review_length\" : ${uiState.data.description.length}," + + "\"photo_count\" : ${uiState.data.photoUrlList.size}," + + "\"has_disappointment\" : ${uiState.data.cons.isNotEmpty()}," + + "\"saved_count\" : ${state.addMapCount}," + + "\"is_self_review\" : ${uiState.data.isMine}," + + "\"is_followed_user_review\" : ${state.isFollowing}," + + "\"is_saved_review\" : ${state.isAddMap}}" + ) + if (scoopDialogVisibility) { ScoopDialog( onClickPositive = { @@ -164,6 +184,7 @@ fun PlaceDetailRoute( DropdownOption.EDIT, DropdownOption.DELETE ) + false -> persistentListOf(DropdownOption.REPORT) } Scaffold( @@ -309,9 +330,11 @@ private fun PlaceDetailScreen( DropdownOption.REPORT.name -> { onReportButtonClick() } + DropdownOption.EDIT.name -> { onEditReviewClick() } + DropdownOption.DELETE.name -> { onDeleteReviewClick() } diff --git a/app/src/main/java/com/spoony/spoony/presentation/register/RegisterScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/register/RegisterScreen.kt index b7760e79d..e1c6e210d 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/register/RegisterScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/register/RegisterScreen.kt @@ -65,7 +65,7 @@ fun RegisterRoute( LaunchedEffect(Unit) { viewModel.loadState() - if(viewModel.registerType == RegisterType.CREATE) { + if (viewModel.registerType == RegisterType.CREATE) { tracker.track("tab_entered", "{\"tab_name\" : \"upload\"}") } } diff --git a/app/src/main/java/com/spoony/spoony/presentation/userpage/mypage/MyPageRoute.kt b/app/src/main/java/com/spoony/spoony/presentation/userpage/mypage/MyPageRoute.kt index cef7d77e3..db02c141b 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/userpage/mypage/MyPageRoute.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/userpage/mypage/MyPageRoute.kt @@ -42,7 +42,7 @@ fun MyPageRoute( viewModel.getUserProfile() viewModel.getSpoonCount() - if(userPageState.userType == UserType.MY_PAGE) { + if (userPageState.userType == UserType.MY_PAGE) { tracker.track("tab_entered", "{\"tab_name\" : \"mypage\"}") } } From f4d6ec894804a0f3898277cb4afb08655e608be2 Mon Sep 17 00:00:00 2001 From: Hyobeen-Park Date: Fri, 3 Oct 2025 03:59:40 +0900 Subject: [PATCH 08/31] =?UTF-8?q?[FEAT/#386]=20=EB=82=B4=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EC=88=98=EC=A0=95=20=EC=99=84=EB=A3=8C=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/register/RegisterEndScreen.kt | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/spoony/spoony/presentation/register/RegisterEndScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/register/RegisterEndScreen.kt index 47edcc23e..43538aab3 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/register/RegisterEndScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/register/RegisterEndScreen.kt @@ -24,6 +24,7 @@ import com.airbnb.lottie.compose.LottieCompositionSpec import com.airbnb.lottie.compose.LottieConstants import com.airbnb.lottie.compose.rememberLottieComposition import com.spoony.spoony.R +import com.spoony.spoony.core.analytics.LocalTracker import com.spoony.spoony.core.designsystem.component.dialog.SingleButtonDialog import com.spoony.spoony.core.designsystem.component.textfield.SpoonyLargeTextField import com.spoony.spoony.core.designsystem.theme.SpoonyAndroidTheme @@ -48,6 +49,8 @@ fun RegisterEndRoute( viewModel: RegisterViewModel, modifier: Modifier = Modifier ) { + val tracker = LocalTracker.current + val state by viewModel.state.collectAsStateWithLifecycle() val registerType = viewModel.registerType @@ -68,7 +71,20 @@ fun RegisterEndRoute( onOptionalReviewChange = viewModel::updateOptionalReview, onRegisterPost = viewModel::registerPost, onRegisterComplete = onRegisterComplete, - onEditComplete = onEditComplete, + onEditComplete = { postId -> + onEditComplete(postId) + tracker.track( + eventName = "review_edited", + properties = "{\"review_id\" : \"$postId\", " + + "\"place_name\" : \"${state.selectedPlace.placeName}\", " + + "\"category\" : \"${state.selectedCategory.categoryName}\", " + + "\"menu_count\" : ${state.menuList.size}, " + + "\"satisfaction_score\" : ${state.userSatisfactionValue}, " + + "\"review_length\" : ${state.detailReview.length}, " + + "\"photo_count\" : ${state.selectedPhotos.size}, " + + "\"has_disappointment\" : ${state.optionalReview.isNotEmpty()}}" + ) + }, postId = viewModel.postId, modifier = modifier ) From 5781b8a79aa605cc4632de597cdb29c9d8d50634 Mon Sep 17 00:00:00 2001 From: Hyobeen-Park Date: Fri, 3 Oct 2025 04:16:15 +0900 Subject: [PATCH 09/31] =?UTF-8?q?[FEAT/#386]=20=EC=9C=A0=EC=A0=80/?= =?UTF-8?q?=EB=A7=88=EC=9D=B4=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=A7=84?= =?UTF-8?q?=EC=9E=85=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/userpage/component/UserScreen.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/app/src/main/java/com/spoony/spoony/presentation/userpage/component/UserScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/userpage/component/UserScreen.kt index 00b7ee5c2..fae848978 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/userpage/component/UserScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/userpage/component/UserScreen.kt @@ -17,6 +17,7 @@ import androidx.compose.foundation.lazy.items import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -27,6 +28,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.spoony.spoony.R +import com.spoony.spoony.core.analytics.LocalTracker import com.spoony.spoony.core.designsystem.component.card.ReviewCard import com.spoony.spoony.core.designsystem.component.dialog.TwoButtonDialog import com.spoony.spoony.core.designsystem.component.screen.EmptyContent @@ -55,10 +57,23 @@ fun UserPageScreen( paddingValues: PaddingValues, modifier: Modifier = Modifier ) { + val tracker = LocalTracker.current + var isReviewDeleteDialogVisible by remember { mutableStateOf(false) } var isUserBlockDialogVisible by remember { mutableStateOf(false) } val topBarMenuItemList = persistentListOf("차단하기", "신고하기") + LaunchedEffect(state.profileId != 0) { + if (state.profileId != 0) { + tracker.track( + eventName = "profile_viewed", + properties = "{\"profile_user_id\" : \"${state.profileId}\", " + + "\"is_self_profile\" : ${state.userType == UserType.MY_PAGE}, " + + "\"is_following_profile_user\" : \"${state.profile.isFollowing}\"}" + ) + } + } + LazyColumn( modifier = modifier .fillMaxSize() From 1364f731f32369ee275b29bf76ad928d10c906fb Mon Sep 17 00:00:00 2001 From: Hyobeen-Park Date: Fri, 3 Oct 2025 04:32:28 +0900 Subject: [PATCH 10/31] =?UTF-8?q?[FEAT/#386]=20=EC=98=A8=EB=B3=B4=EB=94=A9?= =?UTF-8?q?=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/auth/onboarding/OnboardingScreen.kt | 4 ++++ .../auth/onboarding/OnboardingStepOneScreen.kt | 7 ++++++- .../auth/onboarding/OnboardingStepThreeScreen.kt | 10 +++++++++- .../auth/onboarding/OnboardingStepTwoScreen.kt | 11 ++++++++++- 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingScreen.kt index 39b63797f..8a1dc4981 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingScreen.kt @@ -17,6 +17,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController import androidx.navigation.navOptions +import com.spoony.spoony.core.analytics.LocalTracker import com.spoony.spoony.core.designsystem.component.topappbar.SpoonyBasicTopAppBar import com.spoony.spoony.core.designsystem.event.LocalSnackBarTrigger import com.spoony.spoony.core.designsystem.theme.SpoonyAndroidTheme @@ -47,6 +48,7 @@ private fun OnboardingScreen( val navController = rememberNavController() val state by viewModel.state.collectAsStateWithLifecycle() val showSnackbar = LocalSnackBarTrigger.current + val tracker = LocalTracker.current when (state.signUpState) { is UiState.Empty -> { @@ -79,6 +81,7 @@ private fun OnboardingScreen( onBackButtonClick = navController::navigateUp, onSkipButtonClick = { viewModel.skipStep() + tracker.track("onboard_2_skipped") navController.navigate(OnboardingRoute.StepThree) } ) @@ -90,6 +93,7 @@ private fun OnboardingScreen( onSkipButtonClick = { viewModel.skipStep() viewModel.signUp() + tracker.track("onboard_3_skipped") } ) } diff --git a/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingStepOneScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingStepOneScreen.kt index 177eefde9..5d0dd5445 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingStepOneScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingStepOneScreen.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle +import com.spoony.spoony.core.analytics.LocalTracker import com.spoony.spoony.core.designsystem.component.textfield.NicknameTextFieldState import com.spoony.spoony.core.designsystem.component.textfield.SpoonyNicknameTextField import com.spoony.spoony.core.designsystem.event.LocalSnackBarTrigger @@ -30,6 +31,7 @@ fun OnBoardingStepOneRoute( val state by viewModel.state.collectAsStateWithLifecycle() val lifecycleOwner = LocalLifecycleOwner.current val showSnackbar = LocalSnackBarTrigger.current + val tracker = LocalTracker.current LaunchedEffect(Unit) { viewModel.updateCurrentStep(OnboardingSteps.ONE) @@ -52,7 +54,10 @@ fun OnBoardingStepOneRoute( onNicknameChanged = viewModel::updateNickname, onStateChanged = viewModel::updateNicknameState, checkNicknameValid = viewModel::checkUserNameExist, - onButtonClick = onNextButtonClick + onButtonClick = { + onNextButtonClick() + tracker.track("onboard_1_completed") + } ) } diff --git a/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingStepThreeScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingStepThreeScreen.kt index b603c26ce..8e485a2f1 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingStepThreeScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingStepThreeScreen.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.spoony.spoony.core.analytics.LocalTracker import com.spoony.spoony.core.designsystem.component.textfield.SpoonyLargeTextField import com.spoony.spoony.core.designsystem.theme.SpoonyAndroidTheme import com.spoony.spoony.core.util.extension.addFocusCleaner @@ -22,6 +23,7 @@ fun OnboardingStepThreeRoute( viewModel: OnboardingViewModel ) { val state by viewModel.state.collectAsStateWithLifecycle() + val tracker = LocalTracker.current LaunchedEffect(Unit) { viewModel.updateCurrentStep(OnboardingSteps.THREE) @@ -30,7 +32,13 @@ fun OnboardingStepThreeRoute( OnboardingStepThreeScreen( introduction = state.introduction, onValueChanged = viewModel::updateIntroduction, - onButtonClick = viewModel::signUp + onButtonClick = { + viewModel.signUp() + tracker.track( + "onboard_3_completed", + properties = "{\"bio_length\" : ${state.introduction?.length ?: 0}}" + ) + } ) } diff --git a/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingStepTwoScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingStepTwoScreen.kt index dc141e51a..5f14cbfb8 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingStepTwoScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingStepTwoScreen.kt @@ -16,6 +16,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.spoony.spoony.core.analytics.LocalTracker import com.spoony.spoony.core.designsystem.component.bottomsheet.SpoonyDatePickerBottomSheet import com.spoony.spoony.core.designsystem.component.bottomsheet.SpoonyRegionBottomSheet import com.spoony.spoony.core.designsystem.component.button.RegionSelectButton @@ -36,6 +37,7 @@ fun OnboardingStepTwoRoute( ) { val state by viewModel.state.collectAsStateWithLifecycle() val showSnackbar = LocalSnackBarTrigger.current + val tracker = LocalTracker.current var isButtonEnabled by remember { mutableStateOf(false) } var birthBottomSheetVisibility by remember { mutableStateOf(false) } @@ -66,7 +68,14 @@ fun OnboardingStepTwoRoute( regionBottomSheetVisibility = true viewModel.getRegionList() }, - onNextButtonClick = onNextButtonClick + onNextButtonClick = { + onNextButtonClick() + tracker.track( + "onboard_2_completed", + properties = "{\"birthdate_entered\" : ${!state.birth.isNullOrBlank()}," + + "\"region_entered\" : ${state.region != null}}" + ) + } ) if (birthBottomSheetVisibility) { From f3a852fc372e38c577a94d011ff449d9fd07adf5 Mon Sep 17 00:00:00 2001 From: Hyobeen-Park Date: Fri, 3 Oct 2025 04:37:11 +0900 Subject: [PATCH 11/31] =?UTF-8?q?[FEAT/#386]=20=EC=8A=A4=ED=91=BC=EB=BD=91?= =?UTF-8?q?=EA=B8=B0=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/designsystem/component/dialog/SpoonDrawDialog.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/main/java/com/spoony/spoony/core/designsystem/component/dialog/SpoonDrawDialog.kt b/app/src/main/java/com/spoony/spoony/core/designsystem/component/dialog/SpoonDrawDialog.kt index b52d38260..621fac3c5 100644 --- a/app/src/main/java/com/spoony/spoony/core/designsystem/component/dialog/SpoonDrawDialog.kt +++ b/app/src/main/java/com/spoony/spoony/core/designsystem/component/dialog/SpoonDrawDialog.kt @@ -19,6 +19,7 @@ import com.airbnb.lottie.compose.LottieCompositionSpec import com.airbnb.lottie.compose.rememberLottieAnimatable import com.airbnb.lottie.compose.rememberLottieComposition import com.spoony.spoony.R +import com.spoony.spoony.core.analytics.LocalTracker import com.spoony.spoony.core.designsystem.component.image.SpoonyImage import com.spoony.spoony.core.designsystem.model.SpoonDrawModel import com.spoony.spoony.core.designsystem.theme.SpoonyAndroidTheme @@ -34,6 +35,7 @@ fun SpoonDrawDialog( onSpoonDrawButtonClick: suspend () -> SpoonDrawModel, onConfirmButtonClick: () -> Unit ) { + val tracker = LocalTracker.current val coroutineScope = rememberCoroutineScope() var dialogState by remember { mutableStateOf(SpoonDrawDialogState.DRAW) } @@ -88,6 +90,11 @@ fun SpoonDrawDialog( } SpoonDrawDialogState.RESULT -> { + tracker.track( + eventName = "spoon_received", + properties = "{\"spoon_count\" : ${drawResult.spoonAmount}}" + ) + TitleButtonDialog( title = "${drawResult.spoonName} 획득", description = "축하해요!\n총 ${drawResult.spoonAmount}개의 스푼을 적립했어요.", From 75e710f62c934570e7401e7f1ca88bc51841c9bf Mon Sep 17 00:00:00 2001 From: Hyobeen-Park Date: Sat, 4 Oct 2025 01:16:56 +0900 Subject: [PATCH 12/31] =?UTF-8?q?[FEAT/#386]=20=EB=93=B1=EB=A1=9D=20?= =?UTF-8?q?=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/register/RegisterEndScreen.kt | 11 ++++++++++- .../presentation/register/RegisterStartScreen.kt | 13 ++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/spoony/spoony/presentation/register/RegisterEndScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/register/RegisterEndScreen.kt index 43538aab3..f8c385609 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/register/RegisterEndScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/register/RegisterEndScreen.kt @@ -69,7 +69,16 @@ fun RegisterEndRoute( onDetailReviewChange = viewModel::updateDetailReview, onPhotosSelected = viewModel::updatePhotos, onOptionalReviewChange = viewModel::updateOptionalReview, - onRegisterPost = viewModel::registerPost, + onRegisterPost = { + viewModel.registerPost(it) + + tracker.track( + eventName = "review_2_completed", + properties = "{\"review_length\" : ${state.detailReview.length}, " + + "\"photo_count\" : ${state.selectedPhotos.size}, " + + "\"has_disappointment\" : ${state.optionalReview.isNotEmpty()}}" + ) + }, onRegisterComplete = onRegisterComplete, onEditComplete = { postId -> onEditComplete(postId) diff --git a/app/src/main/java/com/spoony/spoony/presentation/register/RegisterStartScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/register/RegisterStartScreen.kt index 4d3279fd0..5c9ba5cf3 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/register/RegisterStartScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/register/RegisterStartScreen.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.spoony.spoony.core.analytics.LocalTracker import com.spoony.spoony.core.designsystem.component.chip.IconChip import com.spoony.spoony.core.designsystem.component.slider.SpoonySlider import com.spoony.spoony.core.designsystem.component.textfield.SpoonyIconButtonTextField @@ -62,6 +63,8 @@ fun RegisterStartRoute( viewModel: RegisterViewModel, modifier: Modifier = Modifier ) { + val tracker = LocalTracker.current + val state by viewModel.state.collectAsStateWithLifecycle() val registerType = viewModel.registerType @@ -80,7 +83,15 @@ fun RegisterStartRoute( RegisterStartScreen( state = state, isNextButtonEnabled = isNextButtonEnabled, - onNextClick = onNextClick, + onNextClick = { + onNextClick() + tracker.track( + eventName = "review_1_completed", + properties = "{\"placeName\" : \"${state.selectedPlace.placeName}\", " + + "\"category\" : \"${state.selectedCategory}\", " + + "\"menu_count\" : ${state.menuList.size}}" + ) + }, onSearchQueryChange = viewModel::updateSearchQuery, onSearchAction = viewModel::searchPlace, onPlaceSelect = viewModel::selectPlace, From cade0fca5c1f6197d18f5a654b493d32612cce6e Mon Sep 17 00:00:00 2001 From: Hyobeen-Park Date: Sat, 4 Oct 2025 03:07:00 +0900 Subject: [PATCH 13/31] =?UTF-8?q?[FEAT/#386]=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spoony/core/analytics/MixPanelTracker.kt | 7 +++ .../presentation/gourmet/map/MapScreen.kt | 2 +- .../presentation/splash/SplashScreen.kt | 41 +++++++++++++-- .../presentation/splash/SplashSideEffect.kt | 6 +++ .../presentation/splash/SplashViewModel.kt | 50 ++++++++++++++++++- .../presentation/splash/model/UserModel.kt | 31 ++++++++++++ 6 files changed, 129 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/com/spoony/spoony/presentation/splash/SplashSideEffect.kt create mode 100644 app/src/main/java/com/spoony/spoony/presentation/splash/model/UserModel.kt diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt b/app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt index 7d78c750c..b94aa97fe 100644 --- a/app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt +++ b/app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt @@ -22,6 +22,13 @@ class MixPanelTracker @Inject constructor( false ) + fun setUserProfile(userId: String, properties: Map) { + mixpanel.identify(userId) + properties.forEach { (key, value) -> + mixpanel.people.set(key, value) + } + } + fun track(eventName: String) { Timber.tag("mixpanel").d(eventName) mixpanel.track(eventName) diff --git a/app/src/main/java/com/spoony/spoony/presentation/gourmet/map/MapScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/gourmet/map/MapScreen.kt index cf8fa7dd2..6c0573574 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/gourmet/map/MapScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/gourmet/map/MapScreen.kt @@ -178,6 +178,7 @@ fun MapRoute( with(state.locationModel) { LaunchedEffect(placeId) { if (placeId == null) { + tracker.track("tab_entered", "{\"tab_name\" : \"map\"}") viewModel.getAddedPlaceList(DEFAULT_CATEGORY_ID) } else { viewModel.getAddedPlaceListByLocation(locationId = placeId) @@ -195,7 +196,6 @@ fun MapRoute( } LaunchedEffect(Unit) { - tracker.track("tab_entered", "{\"tab_name\" : \"map\"}") when { state.locationModel.placeId != null -> { moveCamera( diff --git a/app/src/main/java/com/spoony/spoony/presentation/splash/SplashScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/splash/SplashScreen.kt index 85dc506bf..25773b746 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/splash/SplashScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/splash/SplashScreen.kt @@ -10,12 +10,16 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.flowWithLifecycle import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.spoony.spoony.R import com.spoony.spoony.core.analytics.LocalTracker @@ -29,20 +33,47 @@ fun SplashRoute( viewModel: SplashViewModel = hiltViewModel() ) { val systemUiController = rememberSystemUiController() + val lifecycleOwner = LocalLifecycleOwner.current val tracker = LocalTracker.current + val state by viewModel.state.collectAsStateWithLifecycle() + LaunchedEffect(Unit) { systemUiController.setNavigationBarColor( color = main400 ) tracker.track("app_open") + } - if (viewModel.hasAccessToken()) { - navigateToMap() - } else { - navigateToSignIn() - } + LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { + viewModel.sideEffect.flowWithLifecycle(lifecycleOwner.lifecycle) + .collect { sideEffect -> + when (sideEffect) { + is SplashSideEffect.NavigateToMap -> { + navigateToMap() + + state?.let { + tracker.setUserProfile( + userId = it.userId.toString(), + properties = mapOf( + Pair("login_method", it.platform), + Pair("nickname", it.userName), + Pair("active_region", it.regionName.orEmpty()), + Pair("has_bio", !it.introduction.isNullOrBlank()), + Pair("total_review_count", it.reviewCount), + Pair("follower_count", it.followerCount), + Pair("following_count", it.followingCount), + Pair("signup_date", it.createdAt), + Pair("last_active_date", it.lastEnteredDate.orEmpty()) + ) + ) + } + } + + is SplashSideEffect.NavigateToSignIn -> navigateToSignIn() + } + } } SplashScreen() diff --git a/app/src/main/java/com/spoony/spoony/presentation/splash/SplashSideEffect.kt b/app/src/main/java/com/spoony/spoony/presentation/splash/SplashSideEffect.kt new file mode 100644 index 000000000..8138dd67c --- /dev/null +++ b/app/src/main/java/com/spoony/spoony/presentation/splash/SplashSideEffect.kt @@ -0,0 +1,6 @@ +package com.spoony.spoony.presentation.splash + +sealed class SplashSideEffect { + data object NavigateToMap: SplashSideEffect() + data object NavigateToSignIn: SplashSideEffect() +} diff --git a/app/src/main/java/com/spoony/spoony/presentation/splash/SplashViewModel.kt b/app/src/main/java/com/spoony/spoony/presentation/splash/SplashViewModel.kt index 7dbbcf309..3f3e6e282 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/splash/SplashViewModel.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/splash/SplashViewModel.kt @@ -2,20 +2,66 @@ package com.spoony.spoony.presentation.splash import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.spoony.spoony.core.util.extension.onLogFailure +import com.spoony.spoony.domain.repository.SpoonRepository import com.spoony.spoony.domain.repository.TokenRepository +import com.spoony.spoony.domain.repository.UserRepository +import com.spoony.spoony.presentation.splash.model.UserModel +import com.spoony.spoony.presentation.splash.model.toModel import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @HiltViewModel class SplashViewModel @Inject constructor( - private val tokenRepository: TokenRepository + private val tokenRepository: TokenRepository, + private val userRepository: UserRepository, + private val spoonRepository: SpoonRepository ) : ViewModel() { + private var _state: MutableStateFlow = MutableStateFlow(null) + val state: StateFlow + get() = _state.asStateFlow() + + private val _sideEffect: MutableSharedFlow = MutableSharedFlow() + val sideEffect: SharedFlow + get() = _sideEffect.asSharedFlow() + init { viewModelScope.launch { tokenRepository.initCachedAccessToken() + + if(hasAccessToken()) { + getUserInfo() + } else { + _sideEffect.emit(SplashSideEffect.NavigateToSignIn) + } + } + } + + private suspend fun hasAccessToken(): Boolean = tokenRepository.getAccessToken().first().isNotBlank() + + private fun getUserInfo() { + viewModelScope.launch { + val lastEnteredDate = spoonRepository.getSpoonDrawLog().first + + userRepository.getMyInfo() + .onSuccess { response -> + _state.update { + response.toModel(lastEnteredDate) + } + _sideEffect.emit(SplashSideEffect.NavigateToMap) + } + .onLogFailure { + _sideEffect.emit(SplashSideEffect.NavigateToSignIn) + } } } - suspend fun hasAccessToken(): Boolean = tokenRepository.getAccessToken().first().isNotBlank() } diff --git a/app/src/main/java/com/spoony/spoony/presentation/splash/model/UserModel.kt b/app/src/main/java/com/spoony/spoony/presentation/splash/model/UserModel.kt new file mode 100644 index 000000000..961f0aad4 --- /dev/null +++ b/app/src/main/java/com/spoony/spoony/presentation/splash/model/UserModel.kt @@ -0,0 +1,31 @@ +package com.spoony.spoony.presentation.splash.model + +import com.spoony.spoony.domain.entity.BasicUserInfoEntity + +data class UserModel( + val userId: Int, + val platform: String, + val userName: String, + val regionName: String?, + val introduction: String?, + val createdAt: String, + val updatedAt: String, + val followerCount: Int, + val followingCount: Int, + val reviewCount: Int, + val lastEnteredDate: String? +) + +internal fun BasicUserInfoEntity.toModel(lastEnteredDate: String?) = UserModel( + userId = this.userId, + platform = this.platform, + userName = this.userName, + regionName = this.regionName, + introduction = this.introduction, + createdAt = this.createdAt, + updatedAt = this.updatedAt, + followerCount = this.followerCount, + followingCount = this.followingCount, + reviewCount = this.reviewCount, + lastEnteredDate = lastEnteredDate +) \ No newline at end of file From 9839722ab9b7a538c53357d1a8ee463e307b1058 Mon Sep 17 00:00:00 2001 From: Hyobeen-Park Date: Sat, 4 Oct 2025 04:49:49 +0900 Subject: [PATCH 14/31] =?UTF-8?q?[MOD/#386]=20=EA=B5=AC=EC=A1=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 --- .../spoony/core/analytics/MixPanelTracker.kt | 6 +---- .../core/analytics/events/AnalyticsEvents.kt | 27 +++++++++++++++++++ .../core/analytics/events/MixPanelEvents.kt | 13 +++++++++ .../events/MixPanelUserProperties.kt | 12 +++++++++ .../spoony/presentation/MainActivity.kt | 8 +++--- .../auth/onboarding/OnboardingEndScreen.kt | 4 +-- .../presentation/auth/signin/SignInScreen.kt | 5 ++-- .../presentation/splash/SplashScreen.kt | 6 ++--- 8 files changed, 65 insertions(+), 16 deletions(-) create mode 100644 app/src/main/java/com/spoony/spoony/core/analytics/events/AnalyticsEvents.kt create mode 100644 app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelEvents.kt create mode 100644 app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelUserProperties.kt diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt b/app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt index b94aa97fe..feffb4143 100644 --- a/app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt +++ b/app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt @@ -3,16 +3,12 @@ package com.spoony.spoony.core.analytics import android.content.Context import androidx.compose.runtime.staticCompositionLocalOf import com.mixpanel.android.mpmetrics.MixpanelAPI -import com.spoony.spoony.BuildConfig.MIXPANEL_KEY +import com.spoony.spoony.BuildConfig import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import org.json.JSONObject import timber.log.Timber -val LocalTracker = staticCompositionLocalOf { - error("No MixpanelTracker provided") -} - class MixPanelTracker @Inject constructor( @ApplicationContext private val context: Context ) { diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/events/AnalyticsEvents.kt b/app/src/main/java/com/spoony/spoony/core/analytics/events/AnalyticsEvents.kt new file mode 100644 index 000000000..e2ee57853 --- /dev/null +++ b/app/src/main/java/com/spoony/spoony/core/analytics/events/AnalyticsEvents.kt @@ -0,0 +1,27 @@ +package com.spoony.spoony.core.analytics.events + +import com.spoony.spoony.core.analytics.MixPanelTracker +import jakarta.inject.Inject + +class AnalyticsEvents @Inject constructor( + private val tracker: MixPanelTracker +) { + fun appOpen() { + tracker.track("app_open") + } + + fun signupCompleted(signupMethod: String) { + tracker.track( + eventName = "signup_completed", + properties = """ + { + "signup_method": "$signupMethod" + } + """.trimIndent() + ) + } + + fun loginSuccess() { + tracker.track("login_success") + } +} diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelEvents.kt b/app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelEvents.kt new file mode 100644 index 000000000..e74ee938d --- /dev/null +++ b/app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelEvents.kt @@ -0,0 +1,13 @@ +package com.spoony.spoony.core.analytics.events + +import androidx.compose.runtime.staticCompositionLocalOf +import jakarta.inject.Inject + +val LocalTracker = staticCompositionLocalOf { + error("No MixPanelEvents provided") +} + +class MixPanelEvents @Inject constructor( + val userProperties: MixPanelUserProperties, + val analyticsEvents: AnalyticsEvents +) \ No newline at end of file diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelUserProperties.kt b/app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelUserProperties.kt new file mode 100644 index 000000000..9603947fc --- /dev/null +++ b/app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelUserProperties.kt @@ -0,0 +1,12 @@ +package com.spoony.spoony.core.analytics.events + +import com.spoony.spoony.core.analytics.MixPanelTracker +import jakarta.inject.Inject + +class MixPanelUserProperties @Inject constructor( + private val tracker: MixPanelTracker +) { + fun setUserProfile(userId: String, properties: Map) { + tracker.setUserProfile(userId = userId, properties = properties) + } +} diff --git a/app/src/main/java/com/spoony/spoony/presentation/MainActivity.kt b/app/src/main/java/com/spoony/spoony/presentation/MainActivity.kt index 2d5f4109e..d53e22b5b 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/MainActivity.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/MainActivity.kt @@ -7,8 +7,8 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.runtime.CompositionLocalProvider -import com.spoony.spoony.core.analytics.LocalTracker -import com.spoony.spoony.core.analytics.MixPanelTracker +import com.spoony.spoony.core.analytics.events.LocalTracker +import com.spoony.spoony.core.analytics.events.MixPanelEvents import com.spoony.spoony.core.designsystem.theme.SpoonyAndroidTheme import com.spoony.spoony.presentation.main.MainScreen import dagger.hilt.android.AndroidEntryPoint @@ -17,7 +17,7 @@ import javax.inject.Inject @AndroidEntryPoint class MainActivity : ComponentActivity() { @Inject - lateinit var tracker: MixPanelTracker + lateinit var mixPanelEvents: MixPanelEvents @SuppressLint("SourceLockedOrientationActivity") override fun onCreate(savedInstanceState: Bundle?) { @@ -26,7 +26,7 @@ class MainActivity : ComponentActivity() { enableEdgeToEdge() setContent { SpoonyAndroidTheme { - CompositionLocalProvider(LocalTracker provides tracker) { + CompositionLocalProvider(LocalTracker provides mixPanelEvents) { MainScreen() } } diff --git a/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingEndScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingEndScreen.kt index f5c0c0c13..cdd6f653d 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingEndScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingEndScreen.kt @@ -23,7 +23,7 @@ import com.airbnb.lottie.compose.LottieCompositionSpec import com.airbnb.lottie.compose.LottieConstants import com.airbnb.lottie.compose.rememberLottieComposition import com.spoony.spoony.R -import com.spoony.spoony.core.analytics.LocalTracker +import com.spoony.spoony.core.analytics.events.LocalTracker import com.spoony.spoony.core.designsystem.component.button.SpoonyButton import com.spoony.spoony.core.designsystem.theme.SpoonyAndroidTheme import com.spoony.spoony.core.designsystem.type.ButtonSize @@ -40,7 +40,7 @@ fun OnboardingEndRoute( LaunchedEffect(Unit) { viewModel.updateCurrentStep(OnboardingSteps.END) - tracker.track("signup_completed", "{\"signup_method\" : \"kakao\"}") + tracker.analyticsEvents.signupCompleted("kakao") } OnboardingEndScreen( diff --git a/app/src/main/java/com/spoony/spoony/presentation/auth/signin/SignInScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/auth/signin/SignInScreen.kt index 02581dbdc..cb309e24a 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/auth/signin/SignInScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/auth/signin/SignInScreen.kt @@ -25,7 +25,7 @@ import androidx.lifecycle.flowWithLifecycle import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.kakao.sdk.user.UserApiClient import com.spoony.spoony.R -import com.spoony.spoony.core.analytics.LocalTracker +import com.spoony.spoony.core.analytics.events.LocalTracker import com.spoony.spoony.core.designsystem.event.LocalSnackBarTrigger import com.spoony.spoony.core.designsystem.theme.SpoonyAndroidTheme import com.spoony.spoony.core.designsystem.theme.main100 @@ -58,10 +58,11 @@ fun SignInRoute( is SignInSideEffect.ShowSnackBar -> showSnackbar(sideEffect.message) is SignInSideEffect.NavigateToSignUp -> navigateToTermsOfService() is SignInSideEffect.NavigateToMap -> { - tracker.track("login_success") + tracker.analyticsEvents.loginSuccess() navigateToMap() } + is SignInSideEffect.StartKakaoTalkLogin -> { UserApiClient.instance.loginWithKakaoTalk( context = context, diff --git a/app/src/main/java/com/spoony/spoony/presentation/splash/SplashScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/splash/SplashScreen.kt index 25773b746..f16ea16b4 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/splash/SplashScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/splash/SplashScreen.kt @@ -22,7 +22,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.spoony.spoony.R -import com.spoony.spoony.core.analytics.LocalTracker +import com.spoony.spoony.core.analytics.events.LocalTracker import com.spoony.spoony.core.designsystem.theme.SpoonyAndroidTheme import com.spoony.spoony.core.designsystem.theme.main400 @@ -43,7 +43,7 @@ fun SplashRoute( color = main400 ) - tracker.track("app_open") + tracker.analyticsEvents.appOpen() } LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { @@ -54,7 +54,7 @@ fun SplashRoute( navigateToMap() state?.let { - tracker.setUserProfile( + tracker.userProperties.setUserProfile( userId = it.userId.toString(), properties = mapOf( Pair("login_method", it.platform), From 8077f5ab9ee7d64205edf0d051812075f70ad8b1 Mon Sep 17 00:00:00 2001 From: Hyobeen-Park Date: Sun, 5 Oct 2025 01:03:53 +0900 Subject: [PATCH 15/31] =?UTF-8?q?[FEAT/#386]=20common=20events=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spoony/core/analytics/MixPanelTracker.kt | 3 +- .../core/analytics/events/CommonEvents.kt | 218 ++++++++++++++++++ .../core/analytics/events/MixPanelEvents.kt | 3 +- .../presentation/explore/ExploreScreen.kt | 9 +- .../spoony/presentation/follow/FollowRoute.kt | 3 +- .../follow/component/UserListScreen.kt | 39 +++- .../presentation/gourmet/map/MapScreen.kt | 10 +- .../placeDetail/PlaceDetailRoute.kt | 59 +++-- .../presentation/register/RegisterScreen.kt | 4 +- .../userpage/component/UserScreen.kt | 15 +- .../userpage/mypage/MyPageRoute.kt | 5 +- .../userpage/otherpage/OtherPageRoute.kt | 28 ++- 12 files changed, 353 insertions(+), 43 deletions(-) create mode 100644 app/src/main/java/com/spoony/spoony/core/analytics/events/CommonEvents.kt diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt b/app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt index feffb4143..4da05b162 100644 --- a/app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt +++ b/app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt @@ -1,9 +1,8 @@ package com.spoony.spoony.core.analytics import android.content.Context -import androidx.compose.runtime.staticCompositionLocalOf import com.mixpanel.android.mpmetrics.MixpanelAPI -import com.spoony.spoony.BuildConfig +import com.spoony.spoony.BuildConfig.MIXPANEL_KEY import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import org.json.JSONObject diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/events/CommonEvents.kt b/app/src/main/java/com/spoony/spoony/core/analytics/events/CommonEvents.kt new file mode 100644 index 000000000..8ea36799c --- /dev/null +++ b/app/src/main/java/com/spoony/spoony/core/analytics/events/CommonEvents.kt @@ -0,0 +1,218 @@ +package com.spoony.spoony.core.analytics.events + +import com.spoony.spoony.core.analytics.MixPanelTracker +import jakarta.inject.Inject +import org.json.JSONArray + +class CommonEvents @Inject constructor( + private val tracker: MixPanelTracker +) { + fun tabEntered(tabName: String) { + tracker.track( + eventName = "tab_entered", + properties = """ + { + "tab_name": "$tabName" + } + """.trimIndent() + ) + } + + fun reviewViewed( + reviewId: Int, + authorUserId: Int, + placeName: String, +// category: String, + menuCount: Int, + satisfactionScore: Double, + reviewLength: Int, + photoCount: Int, + hasDisappointment: Boolean, + savedCount: Int, + isSelfReview: Boolean, + isFollowedUserReview: Boolean, + isSavedReview: Boolean +// entryPoint: String + ) { + tracker.track( + eventName = "review_viewed", + properties = """ + { + "review_id": $reviewId, + "author_user_id": $authorUserId, + "place_name": "$placeName", + "menu_count": $menuCount, + "satisfaction_score": $satisfactionScore, + "review_length": $reviewLength, + "photo_count": $photoCount, + "has_disappointment": $hasDisappointment, + "saved_count": $savedCount, + "is_self_review": $isSelfReview, + "is_followed_user_review": $isFollowedUserReview, + "is_saved_review": $isSavedReview + } + """.trimIndent() + ) + } + + fun reviewEdited( + reviewId: Int, +// authorUserId: Int, + placeName: String, + category: String, + menuCount: Int, + satisfactionScore: Float, + reviewLength: Int, + photoCount: Int, + hasDisappointment: Boolean +// savedCount: Int, +// entryPoint: String + ) { + tracker.track( + eventName = "review_edited", + properties = """ + { + "review_id": $reviewId, + "place_name": "$placeName", + "category": "$category", + "menu_count": $menuCount, + "satisfaction_score": $satisfactionScore, + "review_length": $reviewLength, + "photo_count": $photoCount, + "has_disappointment": $hasDisappointment + } + """.trimIndent() + ) + } + + fun profileViewed( + profileUserId: Int, + isSelfProfile: Boolean, + isFollowingProfileUser: Boolean +// entryPoint: String + ) { + tracker.track( + eventName = "profile_viewed", + properties = """ + { + "profile_user_id": $profileUserId, + "is_self_profile": $isSelfProfile, + "is_following_profile_user": $isFollowingProfileUser + } + """.trimIndent() + ) + } + + fun followUser( + followedUserId: Int, + entryPoint: String + ) { + tracker.track( + eventName = "follow_user", + properties = """ + { + "followed_user_id": $followedUserId, + "entry_point" : "$entryPoint" + } + """.trimIndent() + ) + } + + fun unfollowUser( + unfollowedUserId: Int, + entryPoint: String + ) { + tracker.track( + eventName = "unfollow_user", + properties = """ + { + "unfollowed_user_id": $unfollowedUserId, + "entry_point": "$entryPoint" + } + """.trimIndent() + ) + } + + fun followUserFromReview( + reviewId: Int, + authorUserId: Int, + placeName: String, +// category: String, + menuCount: Int, + satisfactionScore: Double, + reviewLength: Int, + photoCount: Int, + hasDisappointment: Boolean, + savedCount: Int + ) { + tracker.track( + eventName = "follow_user_from_review", + properties = """ + { + "review_id": $reviewId, + "author_user_id": $authorUserId, + "place_name": "$placeName", + "menu_count": $menuCount, + "satisfaction_score": $satisfactionScore, + "review_length": $reviewLength, + "photo_count": $photoCount, + "has_disappointment": $hasDisappointment, + "saved_count": $savedCount, + "entry_point" : "review" + } + """.trimIndent() + ) + } + + fun unfollowUserFromReview( + reviewId: Int, + authorUserId: Int, + placeName: String, +// category: String, + menuCount: Int, + satisfactionScore: Double, + reviewLength: Int, + photoCount: Int, + hasDisappointment: Boolean, + savedCount: Int + ) { + tracker.track( + eventName = "unfollow_user_from_review", + properties = """ + { + "review_id": $reviewId, + "author_user_id": $authorUserId, + "place_name": "$placeName", + "menu_count": $menuCount, + "satisfaction_score": $satisfactionScore, + "review_length": $reviewLength, + "photo_count": $photoCount, + "has_disappointment": $hasDisappointment, + "saved_count": $savedCount, + "entry_point" : "review" + } + """.trimIndent() + ) + } + + fun filterApplied( + pageApplied: String, + localReviewFilter: Boolean? = null, + regionFilters: List = listOf(), + categoryFilters: List = listOf(), + ageGroupFilters: List = listOf() + ) { + tracker.track( + eventName = "filter_applied", + properties = """ + { + "page_applied": "$pageApplied", + "local_review_filter": ${localReviewFilter}, + "region_filters": ${JSONArray(regionFilters)}, + "category_filters": ${JSONArray(categoryFilters)}, + "age_group_filters": ${JSONArray(ageGroupFilters)} + } + """.trimIndent() + ) + } +} diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelEvents.kt b/app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelEvents.kt index e74ee938d..613419353 100644 --- a/app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelEvents.kt +++ b/app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelEvents.kt @@ -9,5 +9,6 @@ val LocalTracker = staticCompositionLocalOf { class MixPanelEvents @Inject constructor( val userProperties: MixPanelUserProperties, - val analyticsEvents: AnalyticsEvents + val analyticsEvents: AnalyticsEvents, + val commonEvents: CommonEvents ) \ No newline at end of file diff --git a/app/src/main/java/com/spoony/spoony/presentation/explore/ExploreScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/explore/ExploreScreen.kt index 228082bf9..0b893401e 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/explore/ExploreScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/explore/ExploreScreen.kt @@ -45,7 +45,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.repeatOnLifecycle import com.spoony.spoony.R -import com.spoony.spoony.core.analytics.LocalTracker +import com.spoony.spoony.core.analytics.events.LocalTracker import com.spoony.spoony.core.designsystem.component.card.ReviewCard import com.spoony.spoony.core.designsystem.component.dialog.TwoButtonDialog import com.spoony.spoony.core.designsystem.component.pullToRefresh.SpoonyPullToRefreshContainer @@ -95,7 +95,7 @@ fun ExploreRoute( val listState = rememberLazyListState() LaunchedEffect(Unit) { - tracker.track("review_viewed", "{\"tab_name\" : \"explore\"}") + tracker.commonEvents.tabEntered("explore") } LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { @@ -104,6 +104,7 @@ fun ExploreRoute( is ExploreSideEffect.ShowSnackbar -> { showSnackBar(effect.message) } + is ExploreSideEffect.ScrollToTop -> { coroutineScope.launch { listState.scrollToItem(0) @@ -120,10 +121,6 @@ fun ExploreRoute( } } - LaunchedEffect(Unit) { - tracker.track("tab_entered", "{\"tab_name\" : \"explore\"}") - } - with(state) { ExploreScreen( paddingValues = paddingValues, diff --git a/app/src/main/java/com/spoony/spoony/presentation/follow/FollowRoute.kt b/app/src/main/java/com/spoony/spoony/presentation/follow/FollowRoute.kt index ba5750ea9..a5110b02a 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/follow/FollowRoute.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/follow/FollowRoute.kt @@ -169,7 +169,8 @@ private fun FollowScreen( users = users, onUserClick = onUserClick, onMyClick = onMyClick, - onButtonClick = onFollowButtonClick + onButtonClick = onFollowButtonClick, + type = type ) } diff --git a/app/src/main/java/com/spoony/spoony/presentation/follow/component/UserListScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/follow/component/UserListScreen.kt index bca76cade..763c44b47 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/follow/component/UserListScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/follow/component/UserListScreen.kt @@ -8,6 +8,8 @@ import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import com.spoony.spoony.core.analytics.events.LocalTracker +import com.spoony.spoony.presentation.follow.model.FollowType import com.spoony.spoony.presentation.follow.model.UserItemUiState import kotlinx.collections.immutable.ImmutableList @@ -17,8 +19,11 @@ fun UserListScreen( onUserClick: (Int) -> Unit, onMyClick: () -> Unit, onButtonClick: (Int) -> Unit, + type: FollowType, modifier: Modifier = Modifier ) { + val tracker = LocalTracker.current + LazyColumn( modifier = modifier.fillMaxSize(), contentPadding = PaddingValues(horizontal = 20.dp, vertical = 10.dp) @@ -35,7 +40,39 @@ fun UserListScreen( region = if (user.region.isNullOrBlank()) "" else "서울 ${user.region} 스푼", isFollowing = user.isFollowing, onUserClick = { if (user.isMe) onMyClick() else onUserClick(user.userId) }, - onFollowClick = { onButtonClick(user.userId) }, + onFollowClick = { + onButtonClick(user.userId) + + when (type) { + FollowType.FOLLOWER -> { + if (user.isFollowing) { + tracker.commonEvents.unfollowUser( + unfollowedUserId = user.userId, + entryPoint = "followed_list" + ) + } else { + tracker.commonEvents.followUser( + followedUserId = user.userId, + entryPoint = "followed_list" + ) + } + } + + FollowType.FOLLOWING -> { + if (user.isFollowing) { + tracker.commonEvents.unfollowUser( + unfollowedUserId = user.userId, + entryPoint = "following_list" + ) + } else { + tracker.commonEvents.followUser( + followedUserId = user.userId, + entryPoint = "following_list" + ) + } + } + } + }, modifier = Modifier.padding(vertical = 10.dp) ) } diff --git a/app/src/main/java/com/spoony/spoony/presentation/gourmet/map/MapScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/gourmet/map/MapScreen.kt index 6c0573574..32b28d17c 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/gourmet/map/MapScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/gourmet/map/MapScreen.kt @@ -78,7 +78,7 @@ import com.naver.maps.map.compose.NaverMap import com.naver.maps.map.compose.rememberCameraPositionState import com.naver.maps.map.location.FusedLocationSource import com.spoony.spoony.R -import com.spoony.spoony.core.analytics.LocalTracker +import com.spoony.spoony.core.analytics.events.LocalTracker import com.spoony.spoony.core.designsystem.component.bottomsheet.SpoonyAdvancedBottomSheet import com.spoony.spoony.core.designsystem.component.bottomsheet.SpoonyBasicDragHandle import com.spoony.spoony.core.designsystem.component.chip.IconChip @@ -178,7 +178,7 @@ fun MapRoute( with(state.locationModel) { LaunchedEffect(placeId) { if (placeId == null) { - tracker.track("tab_entered", "{\"tab_name\" : \"map\"}") + tracker.commonEvents.tabEntered("map") viewModel.getAddedPlaceList(DEFAULT_CATEGORY_ID) } else { viewModel.getAddedPlaceListByLocation(locationId = placeId) @@ -316,6 +316,7 @@ private fun MapScreen( onGpsButtonClick: () -> Unit, onCategoryClick: (Int) -> Unit ) { + val tracker = LocalTracker.current val density = LocalDensity.current val sheetState = rememberBottomSheetState( @@ -479,6 +480,11 @@ private fun MapScreen( onClick = { selectedCategoryId = categoryId onCategoryClick(categoryId) + + tracker.commonEvents.filterApplied( + pageApplied = "map", + regionFilters = listOf(categoryName) + ) }, isSelected = categoryId == selectedCategoryId, isGradient = true, diff --git a/app/src/main/java/com/spoony/spoony/presentation/placeDetail/PlaceDetailRoute.kt b/app/src/main/java/com/spoony/spoony/presentation/placeDetail/PlaceDetailRoute.kt index 21ff19924..92cbcb4cd 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/placeDetail/PlaceDetailRoute.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/placeDetail/PlaceDetailRoute.kt @@ -38,7 +38,7 @@ import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.repeatOnLifecycle -import com.spoony.spoony.core.analytics.LocalTracker +import com.spoony.spoony.core.analytics.events.LocalTracker import com.spoony.spoony.core.designsystem.component.button.FollowButton import com.spoony.spoony.core.designsystem.component.snackbar.TextSnackbar import com.spoony.spoony.core.designsystem.component.topappbar.TagTopAppBar @@ -140,20 +140,19 @@ fun PlaceDetailRoute( is UiState.Success -> { val postId = (state.reviewId as? UiState.Success)?.data ?: return - tracker.track( - eventName = "review_viewed", - properties = "{\"review_id\" : \"$postId\"," + - "\"author_user_id\" : \"${userProfile.userId}\"," + - "\"place_name\" : \"${uiState.data.placeName}\"," + - "\"menu_count\" : ${uiState.data.menuList.size}," + - "\"satisfaction_score\" : ${uiState.data.value}," + - "\"review_length\" : ${uiState.data.description.length}," + - "\"photo_count\" : ${uiState.data.photoUrlList.size}," + - "\"has_disappointment\" : ${uiState.data.cons.isNotEmpty()}," + - "\"saved_count\" : ${state.addMapCount}," + - "\"is_self_review\" : ${uiState.data.isMine}," + - "\"is_followed_user_review\" : ${state.isFollowing}," + - "\"is_saved_review\" : ${state.isAddMap}}" + tracker.commonEvents.reviewViewed( + reviewId = postId, + authorUserId = userProfile.userId, + placeName = uiState.data.placeName, + menuCount = uiState.data.menuList.size, + satisfactionScore = uiState.data.value, + reviewLength = uiState.data.description.length, + photoCount = uiState.data.photoUrlList.size, + hasDisappointment = uiState.data.cons.isNotEmpty(), + savedCount = state.addMapCount, + isSelfReview = uiState.data.isMine, + isFollowedUserReview = state.isFollowing, + isSavedReview = state.isAddMap ) if (scoopDialogVisibility) { @@ -245,7 +244,35 @@ fun PlaceDetailRoute( userName = userProfile.userName, userRegion = userProfile.userRegion, isFollowing = state.isFollowing, - onFollowButtonClick = { viewModel.onFollowButtonClick(userProfile.userId, state.isFollowing) }, + onFollowButtonClick = { + viewModel.onFollowButtonClick(userProfile.userId, state.isFollowing) + + if (state.isFollowing) { + tracker.commonEvents.unfollowUserFromReview( + reviewId = postId, + authorUserId = userProfile.userId, + placeName = uiState.data.placeName, + menuCount = uiState.data.menuList.size, + satisfactionScore = uiState.data.value, + reviewLength = uiState.data.description.length, + photoCount = uiState.data.photoUrlList.size, + hasDisappointment = uiState.data.cons.isNotEmpty(), + savedCount = state.addMapCount + ) + } else { + tracker.commonEvents.followUserFromReview( + reviewId = postId, + authorUserId = userProfile.userId, + placeName = uiState.data.placeName, + menuCount = uiState.data.menuList.size, + satisfactionScore = uiState.data.value, + reviewLength = uiState.data.description.length, + photoCount = uiState.data.photoUrlList.size, + hasDisappointment = uiState.data.cons.isNotEmpty(), + savedCount = state.addMapCount + ) + } + }, photoUrlList = data.photoUrlList, date = data.createdAt.formatToYearMonthDay(), placeAddress = data.placeAddress, diff --git a/app/src/main/java/com/spoony/spoony/presentation/register/RegisterScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/register/RegisterScreen.kt index e1c6e210d..d376e30d6 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/register/RegisterScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/register/RegisterScreen.kt @@ -23,7 +23,7 @@ import androidx.lifecycle.flowWithLifecycle import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController -import com.spoony.spoony.core.analytics.LocalTracker +import com.spoony.spoony.core.analytics.events.LocalTracker import com.spoony.spoony.core.designsystem.component.topappbar.TitleTopAppBar import com.spoony.spoony.core.designsystem.event.LocalSnackBarTrigger import com.spoony.spoony.core.designsystem.theme.SpoonyAndroidTheme @@ -66,7 +66,7 @@ fun RegisterRoute( LaunchedEffect(Unit) { viewModel.loadState() if (viewModel.registerType == RegisterType.CREATE) { - tracker.track("tab_entered", "{\"tab_name\" : \"upload\"}") + tracker.commonEvents.tabEntered("upload") } } diff --git a/app/src/main/java/com/spoony/spoony/presentation/userpage/component/UserScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/userpage/component/UserScreen.kt index fae848978..df0e476e0 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/userpage/component/UserScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/userpage/component/UserScreen.kt @@ -28,7 +28,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.spoony.spoony.R -import com.spoony.spoony.core.analytics.LocalTracker +import com.spoony.spoony.core.analytics.events.LocalTracker import com.spoony.spoony.core.designsystem.component.card.ReviewCard import com.spoony.spoony.core.designsystem.component.dialog.TwoButtonDialog import com.spoony.spoony.core.designsystem.component.screen.EmptyContent @@ -63,13 +63,12 @@ fun UserPageScreen( var isUserBlockDialogVisible by remember { mutableStateOf(false) } val topBarMenuItemList = persistentListOf("차단하기", "신고하기") - LaunchedEffect(state.profileId != 0) { + LaunchedEffect(state.profileId) { if (state.profileId != 0) { - tracker.track( - eventName = "profile_viewed", - properties = "{\"profile_user_id\" : \"${state.profileId}\", " + - "\"is_self_profile\" : ${state.userType == UserType.MY_PAGE}, " + - "\"is_following_profile_user\" : \"${state.profile.isFollowing}\"}" + tracker.commonEvents.profileViewed( + profileUserId = state.profileId, + isSelfProfile = state.userType == UserType.MY_PAGE, + isFollowingProfileUser = state.profile.isFollowing ) } } @@ -212,7 +211,7 @@ fun UserPageScreen( ) Text( text = "지금은 프로필을 볼 수 없지만, \n" + - "원하시면 차단을 해제할 수 있어요.", + "원하시면 차단을 해제할 수 있어요.", style = SpoonyAndroidTheme.typography.body2m, color = SpoonyAndroidTheme.colors.gray500, textAlign = TextAlign.Center diff --git a/app/src/main/java/com/spoony/spoony/presentation/userpage/mypage/MyPageRoute.kt b/app/src/main/java/com/spoony/spoony/presentation/userpage/mypage/MyPageRoute.kt index db02c141b..c33bb6ed5 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/userpage/mypage/MyPageRoute.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/userpage/mypage/MyPageRoute.kt @@ -10,7 +10,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle -import com.spoony.spoony.core.analytics.LocalTracker +import com.spoony.spoony.core.analytics.events.LocalTracker import com.spoony.spoony.core.designsystem.event.LocalSnackBarTrigger import com.spoony.spoony.core.designsystem.theme.SpoonyAndroidTheme import com.spoony.spoony.presentation.follow.model.FollowType @@ -43,7 +43,7 @@ fun MyPageRoute( viewModel.getSpoonCount() if (userPageState.userType == UserType.MY_PAGE) { - tracker.track("tab_entered", "{\"tab_name\" : \"mypage\"}") + tracker.commonEvents.tabEntered("mypage") } } @@ -53,6 +53,7 @@ fun MyPageRoute( is MyPageSideEffect.ShowSnackbar -> { showSnackBar(effect.message) } + is MyPageSideEffect.ShowError -> { showSnackBar(effect.errorType.description) } diff --git a/app/src/main/java/com/spoony/spoony/presentation/userpage/otherpage/OtherPageRoute.kt b/app/src/main/java/com/spoony/spoony/presentation/userpage/otherpage/OtherPageRoute.kt index d2facb2a1..3182b1783 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/userpage/otherpage/OtherPageRoute.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/userpage/otherpage/OtherPageRoute.kt @@ -11,6 +11,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle +import com.spoony.spoony.core.analytics.events.LocalTracker import com.spoony.spoony.core.designsystem.event.LocalSnackBarTrigger import com.spoony.spoony.core.designsystem.theme.SpoonyAndroidTheme import com.spoony.spoony.presentation.follow.model.FollowType @@ -35,6 +36,7 @@ fun OtherPageRoute( val userPageState by viewModel.state.collectAsStateWithLifecycle() val showSnackBar = LocalSnackBarTrigger.current val lifecycleOwner = LocalLifecycleOwner.current + val tracker = LocalTracker.current BackHandler { if (userPageState.isBlocked) { @@ -54,6 +56,7 @@ fun OtherPageRoute( is OtherPageSideEffect.ShowSnackbar -> { showSnackBar(effect.message) } + is OtherPageSideEffect.ShowErrorSnackbar -> { showSnackBar(effect.errorType.description) } @@ -73,9 +76,30 @@ fun OtherPageRoute( onReviewClick = navigateToReviewDetail, onReportUserClick = navigateToUserReport, onUserBlockClick = viewModel::blockUser, - onMainButtonClick = viewModel::toggleFollow, + onMainButtonClick = { + if (userPageState.isFollowing) { + tracker.commonEvents.unfollowUser( + unfollowedUserId = userPageState.profile.profileId, + entryPoint = "user_profile" + ) + } else { + tracker.commonEvents.followUser( + followedUserId = userPageState.profile.profileId, + entryPoint = "user_profile" + ) + } + + viewModel.toggleFollow() + }, onReportReviewClick = navigateToReviewReport, - onCheckBoxClick = viewModel::toggleLocalReviewOnly + onCheckBoxClick = { + tracker.commonEvents.filterApplied( + pageApplied = "user_profile", + localReviewFilter = userPageState.isLocalReviewOnly, + ) + + viewModel.toggleLocalReviewOnly() + } ) UserPageScreen( From bcfa1c8e992f6dd920fa72c529084638ce023873 Mon Sep 17 00:00:00 2001 From: Hyobeen-Park Date: Sun, 5 Oct 2025 01:14:06 +0900 Subject: [PATCH 16/31] =?UTF-8?q?[FEAT/#386]=20onboarding=20events=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/analytics/events/MixPanelEvents.kt | 3 +- .../core/analytics/events/OnboardingEvents.kt | 46 +++++++++++++++++++ .../auth/onboarding/OnboardingScreen.kt | 6 +-- .../onboarding/OnboardingStepOneScreen.kt | 4 +- .../onboarding/OnboardingStepThreeScreen.kt | 7 +-- .../onboarding/OnboardingStepTwoScreen.kt | 9 ++-- 6 files changed, 59 insertions(+), 16 deletions(-) create mode 100644 app/src/main/java/com/spoony/spoony/core/analytics/events/OnboardingEvents.kt diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelEvents.kt b/app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelEvents.kt index 613419353..b617fd21e 100644 --- a/app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelEvents.kt +++ b/app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelEvents.kt @@ -10,5 +10,6 @@ val LocalTracker = staticCompositionLocalOf { class MixPanelEvents @Inject constructor( val userProperties: MixPanelUserProperties, val analyticsEvents: AnalyticsEvents, - val commonEvents: CommonEvents + val commonEvents: CommonEvents, + val onboardingEvents: OnboardingEvents ) \ No newline at end of file diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/events/OnboardingEvents.kt b/app/src/main/java/com/spoony/spoony/core/analytics/events/OnboardingEvents.kt new file mode 100644 index 000000000..253789078 --- /dev/null +++ b/app/src/main/java/com/spoony/spoony/core/analytics/events/OnboardingEvents.kt @@ -0,0 +1,46 @@ +package com.spoony.spoony.core.analytics.events + +import com.spoony.spoony.core.analytics.MixPanelTracker +import jakarta.inject.Inject + +class OnboardingEvents @Inject constructor( + private val tracker: MixPanelTracker +) { + fun onboard1Completed() { + tracker.track("onboard_1_completed") + } + + fun onboard2Completed( + isBirthdateEntered: Boolean, + isActiveRegionEntered: Boolean, + ) { + tracker.track( + eventName = "onboard_2_completed", + properties = """ + { + "birthdate_entered": $isBirthdateEntered, + "active_region_entered": $isActiveRegionEntered + } + """.trimIndent() + ) + } + + fun onboard2Skipped() { + tracker.track("onboard_2_skipped") + } + + fun onboard3Completed(bioLength: Int) { + tracker.track( + eventName = "onboard_3_completed", + properties = """ + { + "bio_length": $bioLength + } + """.trimIndent() + ) + } + + fun onboard3Skipped() { + tracker.track("onboard_3_skipped") + } +} diff --git a/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingScreen.kt index 8a1dc4981..2f0f9734f 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingScreen.kt @@ -17,7 +17,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController import androidx.navigation.navOptions -import com.spoony.spoony.core.analytics.LocalTracker +import com.spoony.spoony.core.analytics.events.LocalTracker import com.spoony.spoony.core.designsystem.component.topappbar.SpoonyBasicTopAppBar import com.spoony.spoony.core.designsystem.event.LocalSnackBarTrigger import com.spoony.spoony.core.designsystem.theme.SpoonyAndroidTheme @@ -81,7 +81,7 @@ private fun OnboardingScreen( onBackButtonClick = navController::navigateUp, onSkipButtonClick = { viewModel.skipStep() - tracker.track("onboard_2_skipped") + tracker.onboardingEvents.onboard2Skipped() navController.navigate(OnboardingRoute.StepThree) } ) @@ -93,7 +93,7 @@ private fun OnboardingScreen( onSkipButtonClick = { viewModel.skipStep() viewModel.signUp() - tracker.track("onboard_3_skipped") + tracker.onboardingEvents.onboard3Skipped() } ) } diff --git a/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingStepOneScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingStepOneScreen.kt index 5d0dd5445..86cb747b7 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingStepOneScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingStepOneScreen.kt @@ -14,7 +14,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle -import com.spoony.spoony.core.analytics.LocalTracker +import com.spoony.spoony.core.analytics.events.LocalTracker import com.spoony.spoony.core.designsystem.component.textfield.NicknameTextFieldState import com.spoony.spoony.core.designsystem.component.textfield.SpoonyNicknameTextField import com.spoony.spoony.core.designsystem.event.LocalSnackBarTrigger @@ -56,7 +56,7 @@ fun OnBoardingStepOneRoute( checkNicknameValid = viewModel::checkUserNameExist, onButtonClick = { onNextButtonClick() - tracker.track("onboard_1_completed") + tracker.onboardingEvents.onboard1Completed() } ) } diff --git a/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingStepThreeScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingStepThreeScreen.kt index 8e485a2f1..850cef6ad 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingStepThreeScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingStepThreeScreen.kt @@ -11,7 +11,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.spoony.spoony.core.analytics.LocalTracker +import com.spoony.spoony.core.analytics.events.LocalTracker import com.spoony.spoony.core.designsystem.component.textfield.SpoonyLargeTextField import com.spoony.spoony.core.designsystem.theme.SpoonyAndroidTheme import com.spoony.spoony.core.util.extension.addFocusCleaner @@ -34,10 +34,7 @@ fun OnboardingStepThreeRoute( onValueChanged = viewModel::updateIntroduction, onButtonClick = { viewModel.signUp() - tracker.track( - "onboard_3_completed", - properties = "{\"bio_length\" : ${state.introduction?.length ?: 0}}" - ) + tracker.onboardingEvents.onboard3Completed(state.introduction?.length ?: 0) } ) } diff --git a/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingStepTwoScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingStepTwoScreen.kt index 5f14cbfb8..390046dbe 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingStepTwoScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/auth/onboarding/OnboardingStepTwoScreen.kt @@ -16,7 +16,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.spoony.spoony.core.analytics.LocalTracker +import com.spoony.spoony.core.analytics.events.LocalTracker import com.spoony.spoony.core.designsystem.component.bottomsheet.SpoonyDatePickerBottomSheet import com.spoony.spoony.core.designsystem.component.bottomsheet.SpoonyRegionBottomSheet import com.spoony.spoony.core.designsystem.component.button.RegionSelectButton @@ -70,10 +70,9 @@ fun OnboardingStepTwoRoute( }, onNextButtonClick = { onNextButtonClick() - tracker.track( - "onboard_2_completed", - properties = "{\"birthdate_entered\" : ${!state.birth.isNullOrBlank()}," + - "\"region_entered\" : ${state.region != null}}" + tracker.onboardingEvents.onboard2Completed( + isBirthdateEntered = !state.birth.isNullOrBlank(), + isActiveRegionEntered = state.region != null ) } ) From 122a69ce4a848d26c5c541138d6bac1ab5f8cdd8 Mon Sep 17 00:00:00 2001 From: Hyobeen-Park Date: Sun, 5 Oct 2025 01:23:19 +0900 Subject: [PATCH 17/31] [FEAT/#386] spoon draw & register events --- .../core/analytics/events/MixPanelEvents.kt | 6 ++- .../core/analytics/events/RegisterEvents.kt | 42 +++++++++++++++++++ .../core/analytics/events/SpoonDrawEvents.kt | 19 +++++++++ .../component/dialog/SpoonDrawDialog.kt | 7 +--- .../register/RegisterEndScreen.kt | 33 +++++++-------- .../register/RegisterStartScreen.kt | 13 +++--- 6 files changed, 89 insertions(+), 31 deletions(-) create mode 100644 app/src/main/java/com/spoony/spoony/core/analytics/events/RegisterEvents.kt create mode 100644 app/src/main/java/com/spoony/spoony/core/analytics/events/SpoonDrawEvents.kt diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelEvents.kt b/app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelEvents.kt index b617fd21e..649435133 100644 --- a/app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelEvents.kt +++ b/app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelEvents.kt @@ -11,5 +11,7 @@ class MixPanelEvents @Inject constructor( val userProperties: MixPanelUserProperties, val analyticsEvents: AnalyticsEvents, val commonEvents: CommonEvents, - val onboardingEvents: OnboardingEvents -) \ No newline at end of file + val onboardingEvents: OnboardingEvents, + val spoonDrawEvents: SpoonDrawEvents, + val registerEvents: RegisterEvents +) diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/events/RegisterEvents.kt b/app/src/main/java/com/spoony/spoony/core/analytics/events/RegisterEvents.kt new file mode 100644 index 000000000..cf33a8364 --- /dev/null +++ b/app/src/main/java/com/spoony/spoony/core/analytics/events/RegisterEvents.kt @@ -0,0 +1,42 @@ +package com.spoony.spoony.core.analytics.events + +import com.spoony.spoony.core.analytics.MixPanelTracker +import jakarta.inject.Inject + +class RegisterEvents @Inject constructor( + private val tracker: MixPanelTracker +) { + fun review1Completed( + placeName: String, + category: String, + menuCount: Int + ) { + tracker.track( + eventName = "review_1_completed", + properties = """ + { + "place_name": "$placeName", + "category": "$category", + "menu_count": $menuCount + } + """.trimIndent() + ) + } + + fun review2Completed( + reviewLength: Int, + photoCount: Int, + hasDisappointment: Boolean + ) { + tracker.track( + eventName = "review_2_completed", + properties = """ + { + "review_length": $reviewLength, + "photo_count": $photoCount, + "has_disappointment": $hasDisappointment + } + """.trimIndent() + ) + } +} diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/events/SpoonDrawEvents.kt b/app/src/main/java/com/spoony/spoony/core/analytics/events/SpoonDrawEvents.kt new file mode 100644 index 000000000..fec610812 --- /dev/null +++ b/app/src/main/java/com/spoony/spoony/core/analytics/events/SpoonDrawEvents.kt @@ -0,0 +1,19 @@ +package com.spoony.spoony.core.analytics.events + +import com.spoony.spoony.core.analytics.MixPanelTracker +import jakarta.inject.Inject + +class SpoonDrawEvents @Inject constructor( + private val tracker: MixPanelTracker +) { + fun spoonReceived(spoonCount: Int) { + tracker.track( + eventName = "spoon_received", + properties = """ + { + "spoon_count": $spoonCount + } + """.trimIndent() + ) + } +} diff --git a/app/src/main/java/com/spoony/spoony/core/designsystem/component/dialog/SpoonDrawDialog.kt b/app/src/main/java/com/spoony/spoony/core/designsystem/component/dialog/SpoonDrawDialog.kt index 621fac3c5..7b9e0cbc6 100644 --- a/app/src/main/java/com/spoony/spoony/core/designsystem/component/dialog/SpoonDrawDialog.kt +++ b/app/src/main/java/com/spoony/spoony/core/designsystem/component/dialog/SpoonDrawDialog.kt @@ -19,7 +19,7 @@ import com.airbnb.lottie.compose.LottieCompositionSpec import com.airbnb.lottie.compose.rememberLottieAnimatable import com.airbnb.lottie.compose.rememberLottieComposition import com.spoony.spoony.R -import com.spoony.spoony.core.analytics.LocalTracker +import com.spoony.spoony.core.analytics.events.LocalTracker import com.spoony.spoony.core.designsystem.component.image.SpoonyImage import com.spoony.spoony.core.designsystem.model.SpoonDrawModel import com.spoony.spoony.core.designsystem.theme.SpoonyAndroidTheme @@ -90,10 +90,7 @@ fun SpoonDrawDialog( } SpoonDrawDialogState.RESULT -> { - tracker.track( - eventName = "spoon_received", - properties = "{\"spoon_count\" : ${drawResult.spoonAmount}}" - ) + tracker.spoonDrawEvents.spoonReceived(drawResult.spoonAmount) TitleButtonDialog( title = "${drawResult.spoonName} 획득", diff --git a/app/src/main/java/com/spoony/spoony/presentation/register/RegisterEndScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/register/RegisterEndScreen.kt index 46fe6aa73..785bcb601 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/register/RegisterEndScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/register/RegisterEndScreen.kt @@ -24,7 +24,7 @@ import com.airbnb.lottie.compose.LottieCompositionSpec import com.airbnb.lottie.compose.LottieConstants import com.airbnb.lottie.compose.rememberLottieComposition import com.spoony.spoony.R -import com.spoony.spoony.core.analytics.LocalTracker +import com.spoony.spoony.core.analytics.events.LocalTracker import com.spoony.spoony.core.designsystem.component.dialog.SingleButtonDialog import com.spoony.spoony.core.designsystem.component.textfield.SpoonyLargeTextField import com.spoony.spoony.core.designsystem.theme.SpoonyAndroidTheme @@ -72,26 +72,25 @@ fun RegisterEndRoute( onRegisterPost = { viewModel.registerPost(it) - tracker.track( - eventName = "review_2_completed", - properties = "{\"review_length\" : ${state.detailReview.length}, " + - "\"photo_count\" : ${state.selectedPhotos.size}, " + - "\"has_disappointment\" : ${state.optionalReview.isNotEmpty()}}" + tracker.registerEvents.review2Completed( + reviewLength = state.detailReview.length, + photoCount = state.selectedPhotos.size, + hasDisappointment = state.optionalReview.isNotEmpty() ) }, onRegisterComplete = onRegisterComplete, onEditComplete = { postId -> onEditComplete(postId) - tracker.track( - eventName = "review_edited", - properties = "{\"review_id\" : \"$postId\", " + - "\"place_name\" : \"${state.selectedPlace.placeName}\", " + - "\"category\" : \"${state.selectedCategory.categoryName}\", " + - "\"menu_count\" : ${state.menuList.size}, " + - "\"satisfaction_score\" : ${state.userSatisfactionValue}, " + - "\"review_length\" : ${state.detailReview.length}, " + - "\"photo_count\" : ${state.selectedPhotos.size}, " + - "\"has_disappointment\" : ${state.optionalReview.isNotEmpty()}}" + + tracker.commonEvents.reviewEdited( + reviewId = postId, + placeName = state.selectedPlace.placeName, + category = state.selectedCategory.categoryName, + menuCount = state.menuList.size, + satisfactionScore = state.userSatisfactionValue, + reviewLength = state.detailReview.length, + photoCount = state.selectedPhotos.size, + hasDisappointment = state.optionalReview.isNotEmpty() ) }, postId = viewModel.postId, @@ -267,7 +266,7 @@ private fun OptionalReviewSection( value = optionalReview, onValueChanged = onOptionalReviewChange, placeholder = "쉿 사장님 몰래 남기는 솔직 후기! (선택)\n" + - "이 내용은 비공개 처리 돼요.", + "이 내용은 비공개 처리 돼요.", maxLength = MAX_OPTIONAL_REVIEW_LENGTH, maxErrorText = "글자 수 ${MAX_OPTIONAL_REVIEW_LENGTH}자 이하로 입력해 주세요", decorationBoxHeight = 80.dp, diff --git a/app/src/main/java/com/spoony/spoony/presentation/register/RegisterStartScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/register/RegisterStartScreen.kt index 5c9ba5cf3..217770170 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/register/RegisterStartScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/register/RegisterStartScreen.kt @@ -36,7 +36,7 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.spoony.spoony.core.analytics.LocalTracker +import com.spoony.spoony.core.analytics.events.LocalTracker import com.spoony.spoony.core.designsystem.component.chip.IconChip import com.spoony.spoony.core.designsystem.component.slider.SpoonySlider import com.spoony.spoony.core.designsystem.component.textfield.SpoonyIconButtonTextField @@ -84,13 +84,12 @@ fun RegisterStartRoute( state = state, isNextButtonEnabled = isNextButtonEnabled, onNextClick = { - onNextClick() - tracker.track( - eventName = "review_1_completed", - properties = "{\"placeName\" : \"${state.selectedPlace.placeName}\", " + - "\"category\" : \"${state.selectedCategory}\", " + - "\"menu_count\" : ${state.menuList.size}}" + tracker.registerEvents.review1Completed( + placeName = state.selectedPlace.placeName, + category = state.selectedCategory.categoryName, + menuCount = state.menuList.size ) + onNextClick() }, onSearchQueryChange = viewModel::updateSearchQuery, onSearchAction = viewModel::searchPlace, From f6dc277dca6f323b77461becfcd131561e40ff49 Mon Sep 17 00:00:00 2001 From: Hyobeen-Park Date: Sun, 5 Oct 2025 01:47:02 +0900 Subject: [PATCH 18/31] [FEAT/#386] map events --- .../core/analytics/events/CommonEvents.kt | 2 +- .../spoony/core/analytics/events/MapEvents.kt | 23 +++++++++++++++++++ .../core/analytics/events/MixPanelEvents.kt | 1 + .../core/analytics/events/OnboardingEvents.kt | 2 +- .../spoony/data/mapper/LocationMapper.kt | 1 + .../spoony/domain/entity/LocationEntity.kt | 1 + .../gourmet/map/model/LocationModel.kt | 2 ++ .../gourmet/search/MapSearchScreen.kt | 7 ++++++ .../register/RegisterEndScreen.kt | 2 +- .../presentation/splash/SplashSideEffect.kt | 4 ++-- .../presentation/splash/SplashViewModel.kt | 2 +- .../presentation/splash/model/UserModel.kt | 2 +- .../userpage/component/UserScreen.kt | 2 +- .../userpage/otherpage/OtherPageRoute.kt | 2 +- 14 files changed, 44 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/com/spoony/spoony/core/analytics/events/MapEvents.kt diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/events/CommonEvents.kt b/app/src/main/java/com/spoony/spoony/core/analytics/events/CommonEvents.kt index 8ea36799c..1142eea81 100644 --- a/app/src/main/java/com/spoony/spoony/core/analytics/events/CommonEvents.kt +++ b/app/src/main/java/com/spoony/spoony/core/analytics/events/CommonEvents.kt @@ -207,7 +207,7 @@ class CommonEvents @Inject constructor( properties = """ { "page_applied": "$pageApplied", - "local_review_filter": ${localReviewFilter}, + "local_review_filter": $localReviewFilter, "region_filters": ${JSONArray(regionFilters)}, "category_filters": ${JSONArray(categoryFilters)}, "age_group_filters": ${JSONArray(ageGroupFilters)} diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/events/MapEvents.kt b/app/src/main/java/com/spoony/spoony/core/analytics/events/MapEvents.kt new file mode 100644 index 000000000..84b9154b6 --- /dev/null +++ b/app/src/main/java/com/spoony/spoony/core/analytics/events/MapEvents.kt @@ -0,0 +1,23 @@ +package com.spoony.spoony.core.analytics.events + +import com.spoony.spoony.core.analytics.MixPanelTracker +import jakarta.inject.Inject + +class MapEvents @Inject constructor( + private val tracker: MixPanelTracker +) { + fun mapSearched( + locationType: String, + searchTerm: String + ) { + tracker.track( + eventName = "map_searched", + properties = """ + { + "location_type": "$locationType", + "search_term": "$searchTerm" + } + """.trimIndent() + ) + } +} diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelEvents.kt b/app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelEvents.kt index 649435133..6bc8c97ff 100644 --- a/app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelEvents.kt +++ b/app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelEvents.kt @@ -13,5 +13,6 @@ class MixPanelEvents @Inject constructor( val commonEvents: CommonEvents, val onboardingEvents: OnboardingEvents, val spoonDrawEvents: SpoonDrawEvents, + val mapEvents: MapEvents, val registerEvents: RegisterEvents ) diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/events/OnboardingEvents.kt b/app/src/main/java/com/spoony/spoony/core/analytics/events/OnboardingEvents.kt index 253789078..30271459a 100644 --- a/app/src/main/java/com/spoony/spoony/core/analytics/events/OnboardingEvents.kt +++ b/app/src/main/java/com/spoony/spoony/core/analytics/events/OnboardingEvents.kt @@ -12,7 +12,7 @@ class OnboardingEvents @Inject constructor( fun onboard2Completed( isBirthdateEntered: Boolean, - isActiveRegionEntered: Boolean, + isActiveRegionEntered: Boolean ) { tracker.track( eventName = "onboard_2_completed", diff --git a/app/src/main/java/com/spoony/spoony/data/mapper/LocationMapper.kt b/app/src/main/java/com/spoony/spoony/data/mapper/LocationMapper.kt index 6ca3136d7..00725fede 100644 --- a/app/src/main/java/com/spoony/spoony/data/mapper/LocationMapper.kt +++ b/app/src/main/java/com/spoony/spoony/data/mapper/LocationMapper.kt @@ -6,6 +6,7 @@ import com.spoony.spoony.domain.entity.LocationEntity fun LocationListResponseDto.LocationResponseDto.toDomain(): LocationEntity = LocationEntity( locationId = this.locationId, locationName = this.locationName, + locationType = this.locationType.locationTypeName, locationAddress = this.locationAddress, scope = this.locationType.scope, latitude = this.latitude, diff --git a/app/src/main/java/com/spoony/spoony/domain/entity/LocationEntity.kt b/app/src/main/java/com/spoony/spoony/domain/entity/LocationEntity.kt index b86c795b3..17b675b7d 100644 --- a/app/src/main/java/com/spoony/spoony/domain/entity/LocationEntity.kt +++ b/app/src/main/java/com/spoony/spoony/domain/entity/LocationEntity.kt @@ -8,6 +8,7 @@ data class LocationEntity( val locationId: Int, val locationName: String, val locationAddress: String, + val locationType: String, val scope: Double, val latitude: Double, val longitude: Double diff --git a/app/src/main/java/com/spoony/spoony/presentation/gourmet/map/model/LocationModel.kt b/app/src/main/java/com/spoony/spoony/presentation/gourmet/map/model/LocationModel.kt index 3ebe4033e..7387d7148 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/gourmet/map/model/LocationModel.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/gourmet/map/model/LocationModel.kt @@ -6,6 +6,7 @@ data class LocationModel( val placeId: Int? = null, val placeName: String? = null, val locationAddress: String? = null, + val locationType: String? = null, val scale: Double = 14.0, val latitude: Double = 0.0, val longitude: Double = 0.0 @@ -15,6 +16,7 @@ fun LocationEntity.toModel(): LocationModel = LocationModel( placeId = this.locationId, placeName = this.locationName, locationAddress = this.locationAddress, + locationType = this.locationType, scale = this.scope, latitude = this.latitude, longitude = this.longitude diff --git a/app/src/main/java/com/spoony/spoony/presentation/gourmet/search/MapSearchScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/gourmet/search/MapSearchScreen.kt index 864e75ebb..f1df94d65 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/gourmet/search/MapSearchScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/gourmet/search/MapSearchScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.spoony.spoony.core.analytics.events.LocalTracker import com.spoony.spoony.core.designsystem.theme.SpoonyAndroidTheme import com.spoony.spoony.core.state.UiState import com.spoony.spoony.core.util.extension.noRippleClickable @@ -76,6 +77,7 @@ private fun MapSearchScreen( ) { val focusRequester = remember { FocusRequester() } val focusManager = LocalFocusManager.current + val tracker = LocalTracker.current LaunchedEffect(Unit) { focusRequester.requestFocus() @@ -168,6 +170,11 @@ private fun MapSearchScreen( modifier = Modifier .background(SpoonyAndroidTheme.colors.white) .noRippleClickable { + tracker.mapEvents.mapSearched( + locationType = locationInfo.locationType.orEmpty(), + searchTerm = searchKeyword + ) + onResultItemClick( locationInfo.placeId ?: 0, locationInfo.placeName ?: "", diff --git a/app/src/main/java/com/spoony/spoony/presentation/register/RegisterEndScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/register/RegisterEndScreen.kt index 785bcb601..a76ab7a44 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/register/RegisterEndScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/register/RegisterEndScreen.kt @@ -266,7 +266,7 @@ private fun OptionalReviewSection( value = optionalReview, onValueChanged = onOptionalReviewChange, placeholder = "쉿 사장님 몰래 남기는 솔직 후기! (선택)\n" + - "이 내용은 비공개 처리 돼요.", + "이 내용은 비공개 처리 돼요.", maxLength = MAX_OPTIONAL_REVIEW_LENGTH, maxErrorText = "글자 수 ${MAX_OPTIONAL_REVIEW_LENGTH}자 이하로 입력해 주세요", decorationBoxHeight = 80.dp, diff --git a/app/src/main/java/com/spoony/spoony/presentation/splash/SplashSideEffect.kt b/app/src/main/java/com/spoony/spoony/presentation/splash/SplashSideEffect.kt index 8138dd67c..85de85879 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/splash/SplashSideEffect.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/splash/SplashSideEffect.kt @@ -1,6 +1,6 @@ package com.spoony.spoony.presentation.splash sealed class SplashSideEffect { - data object NavigateToMap: SplashSideEffect() - data object NavigateToSignIn: SplashSideEffect() + data object NavigateToMap : SplashSideEffect() + data object NavigateToSignIn : SplashSideEffect() } diff --git a/app/src/main/java/com/spoony/spoony/presentation/splash/SplashViewModel.kt b/app/src/main/java/com/spoony/spoony/presentation/splash/SplashViewModel.kt index 3f3e6e282..e8d036c3b 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/splash/SplashViewModel.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/splash/SplashViewModel.kt @@ -38,7 +38,7 @@ class SplashViewModel @Inject constructor( viewModelScope.launch { tokenRepository.initCachedAccessToken() - if(hasAccessToken()) { + if (hasAccessToken()) { getUserInfo() } else { _sideEffect.emit(SplashSideEffect.NavigateToSignIn) diff --git a/app/src/main/java/com/spoony/spoony/presentation/splash/model/UserModel.kt b/app/src/main/java/com/spoony/spoony/presentation/splash/model/UserModel.kt index 961f0aad4..8d8a86fe8 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/splash/model/UserModel.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/splash/model/UserModel.kt @@ -28,4 +28,4 @@ internal fun BasicUserInfoEntity.toModel(lastEnteredDate: String?) = UserModel( followingCount = this.followingCount, reviewCount = this.reviewCount, lastEnteredDate = lastEnteredDate -) \ No newline at end of file +) diff --git a/app/src/main/java/com/spoony/spoony/presentation/userpage/component/UserScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/userpage/component/UserScreen.kt index df0e476e0..009514fa6 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/userpage/component/UserScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/userpage/component/UserScreen.kt @@ -211,7 +211,7 @@ fun UserPageScreen( ) Text( text = "지금은 프로필을 볼 수 없지만, \n" + - "원하시면 차단을 해제할 수 있어요.", + "원하시면 차단을 해제할 수 있어요.", style = SpoonyAndroidTheme.typography.body2m, color = SpoonyAndroidTheme.colors.gray500, textAlign = TextAlign.Center diff --git a/app/src/main/java/com/spoony/spoony/presentation/userpage/otherpage/OtherPageRoute.kt b/app/src/main/java/com/spoony/spoony/presentation/userpage/otherpage/OtherPageRoute.kt index 3182b1783..ddfeb13d7 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/userpage/otherpage/OtherPageRoute.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/userpage/otherpage/OtherPageRoute.kt @@ -95,7 +95,7 @@ fun OtherPageRoute( onCheckBoxClick = { tracker.commonEvents.filterApplied( pageApplied = "user_profile", - localReviewFilter = userPageState.isLocalReviewOnly, + localReviewFilter = userPageState.isLocalReviewOnly ) viewModel.toggleLocalReviewOnly() From 4ad657e127568b74bb6fbeaf911c13c8e1c85bf2 Mon Sep 17 00:00:00 2001 From: Hyobeen-Park Date: Sun, 5 Oct 2025 02:00:19 +0900 Subject: [PATCH 19/31] [FEAT/#386] explore events --- .../core/analytics/events/ExploreEvents.kt | 34 +++++++++++++++++++ .../core/analytics/events/MixPanelEvents.kt | 1 + .../presentation/explore/ExploreScreen.kt | 6 +++- .../explore/type/SortingOption.kt | 7 ++-- .../exploreSearch/ExploreSearchScreen.kt | 21 ++++++++++-- .../exploreSearch/type/SearchType.kt | 8 +++-- 6 files changed, 67 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/com/spoony/spoony/core/analytics/events/ExploreEvents.kt diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/events/ExploreEvents.kt b/app/src/main/java/com/spoony/spoony/core/analytics/events/ExploreEvents.kt new file mode 100644 index 000000000..90edc06b4 --- /dev/null +++ b/app/src/main/java/com/spoony/spoony/core/analytics/events/ExploreEvents.kt @@ -0,0 +1,34 @@ +package com.spoony.spoony.core.analytics.events + +import com.spoony.spoony.core.analytics.MixPanelTracker +import jakarta.inject.Inject + +class ExploreEvents @Inject constructor( + private val tracker: MixPanelTracker +) { + fun sortSelected(sortType: String) { + tracker.track( + eventName = "sort_selected", + properties = """ + { + "sort_type": "$sortType" + } + """.trimIndent() + ) + } + + fun exploreSearched( + searchTargetType: String, + searchTerm: String + ) { + tracker.track( + eventName = "explore_searched", + properties = """ + { + "search_target_type": "$searchTargetType", + "search_term": "$searchTerm" + } + """.trimIndent() + ) + } +} diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelEvents.kt b/app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelEvents.kt index 6bc8c97ff..ff729d59b 100644 --- a/app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelEvents.kt +++ b/app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelEvents.kt @@ -14,5 +14,6 @@ class MixPanelEvents @Inject constructor( val onboardingEvents: OnboardingEvents, val spoonDrawEvents: SpoonDrawEvents, val mapEvents: MapEvents, + val exploreEvents: ExploreEvents, val registerEvents: RegisterEvents ) diff --git a/app/src/main/java/com/spoony/spoony/presentation/explore/ExploreScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/explore/ExploreScreen.kt index 0b893401e..3725fecb5 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/explore/ExploreScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/explore/ExploreScreen.kt @@ -91,6 +91,7 @@ fun ExploreRoute( val lifecycleOwner = LocalLifecycleOwner.current val showSnackBar = LocalSnackBarTrigger.current val tracker = LocalTracker.current + val coroutineScope = rememberCoroutineScope() val listState = rememberLazyListState() @@ -131,7 +132,10 @@ fun ExploreRoute( onEditButtonClick = navigateToEditReview, onFilterApplyButtonClick = viewModel::applyExploreFilter, onLocalReviewButtonClick = viewModel::localReviewToggle, - onSelectSortingOptionButtonClick = viewModel::updateSelectedSortingOption, + onSelectSortingOptionButtonClick = { sortingOption -> + viewModel.updateSelectedSortingOption(sortingOption) + tracker.exploreEvents.sortSelected(sortingOption.trackingCode) + }, onTabChange = viewModel::updateExploreType, onRefresh = viewModel::refreshExploreScreen, onLoadNextPage = viewModel::getPlaceReviewListFiltered, diff --git a/app/src/main/java/com/spoony/spoony/presentation/explore/type/SortingOption.kt b/app/src/main/java/com/spoony/spoony/presentation/explore/type/SortingOption.kt index f4aa3d1f3..538501dfd 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/explore/type/SortingOption.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/explore/type/SortingOption.kt @@ -2,8 +2,9 @@ package com.spoony.spoony.presentation.explore.type enum class SortingOption( val stringValue: String, - val stringCode: String + val stringCode: String, + val trackingCode: String ) { - LATEST("최신순", "createdAt"), - POPULARITY("저장 많은 순", "zzimCount") + LATEST("최신순", "createdAt", "latest"), + POPULARITY("저장 많은 순", "zzimCount", "most_saved") } diff --git a/app/src/main/java/com/spoony/spoony/presentation/exploreSearch/ExploreSearchScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/exploreSearch/ExploreSearchScreen.kt index aa0f7d46a..61acf862e 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/exploreSearch/ExploreSearchScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/exploreSearch/ExploreSearchScreen.kt @@ -38,6 +38,7 @@ import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.repeatOnLifecycle +import com.spoony.spoony.core.analytics.events.LocalTracker import com.spoony.spoony.core.designsystem.component.card.ReviewCard import com.spoony.spoony.core.designsystem.component.dialog.TwoButtonDialog import com.spoony.spoony.core.designsystem.event.LocalSnackBarTrigger @@ -74,6 +75,7 @@ fun ExploreSearchRoute( val state by viewModel.state.collectAsStateWithLifecycle() val lifecycleOwner = LocalLifecycleOwner.current val showSnackBar = LocalSnackBarTrigger.current + val tracker = LocalTracker.current LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { viewModel.sideEffect.flowWithLifecycle(lifecycleOwner.lifecycle).collect { effect -> @@ -104,7 +106,13 @@ fun ExploreSearchRoute( onSwitchType = viewModel::switchSearchType, onRemoveRecentSearchItem = viewModel::removeRecentSearchItem, onClearRecentSearchItem = viewModel::clearRecentSearchItem, - onSearch = viewModel::search, + onSearch = { keyword -> + viewModel.search(keyword) + tracker.exploreEvents.exploreSearched( + searchTargetType = state.searchType.trackingCode, + searchTerm = keyword + ) + }, onEditReviewClick = navigateToEditReview, onClearSearchKeyword = viewModel::clearSearchKeyword, onReviewDeleteButtonClick = viewModel::deleteReview, @@ -247,6 +255,7 @@ private fun ExploreSearchScreen( ) } } + searchKeyword.isBlank() && searchText.isNotBlank() -> {} else -> { when (userInfoList) { @@ -273,14 +282,17 @@ private fun ExploreSearchScreen( } } } + is UiState.Empty -> { ExploreSearchEmptyScreen(searchType = searchType) } - else -> { } + + else -> {} } } } } + 1 -> { when { searchKeyword.isBlank() && searchText.isBlank() -> { @@ -299,6 +311,7 @@ private fun ExploreSearchScreen( ) } } + searchKeyword.isBlank() && searchText.isNotBlank() -> {} else -> { when (placeReviewInfoList) { @@ -357,10 +370,12 @@ private fun ExploreSearchScreen( } } } + is UiState.Empty -> { ExploreSearchEmptyScreen(searchType = searchType) } - else -> { } + + else -> {} } } } diff --git a/app/src/main/java/com/spoony/spoony/presentation/exploreSearch/type/SearchType.kt b/app/src/main/java/com/spoony/spoony/presentation/exploreSearch/type/SearchType.kt index f30c40e00..fd27768bd 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/exploreSearch/type/SearchType.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/exploreSearch/type/SearchType.kt @@ -1,8 +1,10 @@ package com.spoony.spoony.presentation.exploreSearch.type -enum class SearchType { - USER, - REVIEW +enum class SearchType( + val trackingCode: String +) { + USER("user"), + REVIEW("review") } fun SearchType.toKoreanText(): String { From 03b7a030fbfef1c7c5916892248f6b220ba3daf6 Mon Sep 17 00:00:00 2001 From: Hyobeen-Park Date: Sun, 5 Oct 2025 02:25:23 +0900 Subject: [PATCH 20/31] [FEAT/#386] mypage events --- .../core/analytics/events/MixPanelEvents.kt | 3 ++- .../core/analytics/events/MypageEvents.kt | 24 +++++++++++++++++++ .../profileedit/ProfileEditScreen.kt | 15 ++++++++++-- .../profileedit/ProfileEditViewModel.kt | 9 +++++++ 4 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/spoony/spoony/core/analytics/events/MypageEvents.kt diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelEvents.kt b/app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelEvents.kt index ff729d59b..e158d64f8 100644 --- a/app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelEvents.kt +++ b/app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelEvents.kt @@ -15,5 +15,6 @@ class MixPanelEvents @Inject constructor( val spoonDrawEvents: SpoonDrawEvents, val mapEvents: MapEvents, val exploreEvents: ExploreEvents, - val registerEvents: RegisterEvents + val registerEvents: RegisterEvents, + val mypageEvents: MypageEvents ) diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/events/MypageEvents.kt b/app/src/main/java/com/spoony/spoony/core/analytics/events/MypageEvents.kt new file mode 100644 index 000000000..1f1721ac9 --- /dev/null +++ b/app/src/main/java/com/spoony/spoony/core/analytics/events/MypageEvents.kt @@ -0,0 +1,24 @@ +package com.spoony.spoony.core.analytics.events + +import com.spoony.spoony.core.analytics.MixPanelTracker +import jakarta.inject.Inject +import org.json.JSONArray + +class MypageEvents @Inject constructor( + private val tracker: MixPanelTracker +) { + fun profileUpdated(fieldsUpdated: List = listOf()) { + tracker.track( + eventName = "profile_updated", + properties = """ + { + "fields_updated": ${JSONArray(fieldsUpdated)} + } + """.trimIndent() + ) + } + + fun spoonCharacterViewed() { + tracker.track("spoon_character_viewed") + } +} diff --git a/app/src/main/java/com/spoony/spoony/presentation/profileedit/ProfileEditScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/profileedit/ProfileEditScreen.kt index 38b9cfc39..00e40959c 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/profileedit/ProfileEditScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/profileedit/ProfileEditScreen.kt @@ -36,6 +36,7 @@ import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle import com.spoony.spoony.R +import com.spoony.spoony.core.analytics.events.LocalTracker import com.spoony.spoony.core.designsystem.component.bottomsheet.SpoonyDatePickerBottomSheet import com.spoony.spoony.core.designsystem.component.bottomsheet.SpoonyRegionBottomSheet import com.spoony.spoony.core.designsystem.component.button.RegionSelectButton @@ -61,6 +62,8 @@ fun ProfileEditScreen( modifier: Modifier = Modifier, viewModel: ProfileEditViewModel = hiltViewModel() ) { + val tracker = LocalTracker.current + val profileEditModel by viewModel.profileEditModel.collectAsStateWithLifecycle() val nicknameState by viewModel.nicknameState.collectAsStateWithLifecycle() val saveButtonEnabled by viewModel.saveButtonEnabled.collectAsStateWithLifecycle() @@ -125,7 +128,10 @@ fun ProfileEditScreen( ) Icon( imageVector = ImageVector.vectorResource(id = R.drawable.ic_question_24), - modifier = Modifier.noRippleClickable { isImageBottomSheetVisible = true }, + modifier = Modifier.noRippleClickable { + isImageBottomSheetVisible = true + tracker.mypageEvents.spoonCharacterViewed() + }, tint = Color.Unspecified, contentDescription = null ) @@ -197,7 +203,12 @@ fun ProfileEditScreen( SaveButton( enabled = saveButtonEnabled, - onClick = viewModel::updateProfileInfo, + onClick = { + viewModel.updateProfileInfo() + tracker.mypageEvents.profileUpdated( + fieldsUpdated = viewModel.fieldsUpdated + ) + }, modifier = Modifier.padding(horizontal = 20.dp) ) diff --git a/app/src/main/java/com/spoony/spoony/presentation/profileedit/ProfileEditViewModel.kt b/app/src/main/java/com/spoony/spoony/presentation/profileedit/ProfileEditViewModel.kt index 06fef5864..60618ff22 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/profileedit/ProfileEditViewModel.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/profileedit/ProfileEditViewModel.kt @@ -46,6 +46,10 @@ class ProfileEditViewModel @Inject constructor( val sideEffect: SharedFlow get() = _sideEffect.asSharedFlow() + private val _fieldsUpdated: MutableSet = mutableSetOf() + val fieldsUpdated: List + get() = _fieldsUpdated.toList() + init { loadProfileEditData() } @@ -79,6 +83,7 @@ class ProfileEditViewModel @Inject constructor( fun updateNickname(nickname: String) { _profileEditModel.update { it.copy(userName = nickname) } updateSaveButtonState() + _fieldsUpdated.add("nickname") } fun updateNicknameState(state: NicknameTextFieldState) { @@ -129,6 +134,7 @@ class ProfileEditViewModel @Inject constructor( _profileEditModel.update { it.copy(introduction = introduction.takeIf { it.isNotBlank() }) } + _fieldsUpdated.add("bio") } fun selectImageLevel(level: Int) { @@ -142,6 +148,7 @@ class ProfileEditViewModel @Inject constructor( profileImages = updatedImages ) } + _fieldsUpdated.add("profile_image") } fun selectDate(year: String, month: String, day: String) { @@ -153,6 +160,7 @@ class ProfileEditViewModel @Inject constructor( isBirthSelected = true ) } + _fieldsUpdated.add("birthdate") } fun selectRegion(regionId: Int, regionName: String) { @@ -163,6 +171,7 @@ class ProfileEditViewModel @Inject constructor( isRegionSelected = true ) } + _fieldsUpdated.add("active_region") } fun updateProfileInfo() { From 67682d7f1d91b163cb836e1a61c363b94100308d Mon Sep 17 00:00:00 2001 From: Hyobeen-Park Date: Sun, 5 Oct 2025 02:43:40 +0900 Subject: [PATCH 21/31] [FEAT/#386] review detail events --- .../core/analytics/events/MixPanelEvents.kt | 3 +- .../analytics/events/ReviewDetailEvents.kt | 172 ++++++++++++++++++ .../placeDetail/PlaceDetailRoute.kt | 81 ++++++++- 3 files changed, 251 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/com/spoony/spoony/core/analytics/events/ReviewDetailEvents.kt diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelEvents.kt b/app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelEvents.kt index e158d64f8..4d10e2507 100644 --- a/app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelEvents.kt +++ b/app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelEvents.kt @@ -16,5 +16,6 @@ class MixPanelEvents @Inject constructor( val mapEvents: MapEvents, val exploreEvents: ExploreEvents, val registerEvents: RegisterEvents, - val mypageEvents: MypageEvents + val mypageEvents: MypageEvents, + val reviewDetailEvents: ReviewDetailEvents ) diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/events/ReviewDetailEvents.kt b/app/src/main/java/com/spoony/spoony/core/analytics/events/ReviewDetailEvents.kt new file mode 100644 index 000000000..19654b8ce --- /dev/null +++ b/app/src/main/java/com/spoony/spoony/core/analytics/events/ReviewDetailEvents.kt @@ -0,0 +1,172 @@ +package com.spoony.spoony.core.analytics.events + +import com.spoony.spoony.core.analytics.MixPanelTracker +import jakarta.inject.Inject + +class ReviewDetailEvents @Inject constructor( + private val tracker: MixPanelTracker +) { + fun spoonUseIntent( + reviewId: Int, + authorUserId: Int, + placeName: String, +// category: String, + menuCount: Int, + satisfactionScore: Double, + reviewLength: Int, + photoCount: Int, + hasDisappointment: Boolean, + savedCount: Int, + isFollowingAuthor: Boolean + ) { + tracker.track( + eventName = "spoon_use_intent", + properties = """ + { + "review_id" : $reviewId, + "author_user_id" : $authorUserId, + "place_name" : "$placeName", + "menu_count" : $menuCount, + "satisfaction_score" : $satisfactionScore, + "review_length" : $reviewLength, + "photo_count" : $photoCount, + "has_disappointment" : $hasDisappointment, + "saved_count" : $savedCount, + "is_following_author" : $isFollowingAuthor + } + """.trimIndent() + ) + } + + fun spoonUsed( + reviewId: Int, + authorUserId: Int, + placeName: String, +// category: String, + menuCount: Int, + satisfactionScore: Double, + reviewLength: Int, + photoCount: Int, + hasDisappointment: Boolean, + savedCount: Int, + isFollowingAuthor: Boolean + ) { + tracker.track( + eventName = "spoon_used", + properties = """ + { + "review_id" : $reviewId, + "author_user_id" : $authorUserId, + "place_name" : "$placeName", + "menu_count" : $menuCount, + "satisfaction_score" : $satisfactionScore, + "review_length" : $reviewLength, + "photo_count" : $photoCount, + "has_disappointment" : $hasDisappointment, + "saved_count" : $savedCount, + "is_following_author" : $isFollowingAuthor + } + """.trimIndent() + ) + } + + fun spoonUseFailed() { + tracker.track("spoon_use_failed") + } + + fun placeMapSaved( + reviewId: Int, + authorUserId: Int, + placeName: String, +// category: String, + menuCount: Int, + satisfactionScore: Double, + reviewLength: Int, + photoCount: Int, + hasDisappointment: Boolean, + savedCount: Int, + isFollowingAuthor: Boolean + ) { + tracker.track( + eventName = "place_map_saved", + properties = """ + { + "review_id" : $reviewId, + "author_user_id" : $authorUserId, + "place_name" : "$placeName", + "menu_count" : $menuCount, + "satisfaction_score" : $satisfactionScore, + "review_length" : $reviewLength, + "photo_count" : $photoCount, + "has_disappointment" : $hasDisappointment, + "saved_count" : $savedCount, + "is_following_author" : $isFollowingAuthor + } + """.trimIndent() + ) + } + + fun placeMapRemoved( + reviewId: Int, + authorUserId: Int, + placeName: String, +// category: String, + menuCount: Int, + satisfactionScore: Double, + reviewLength: Int, + photoCount: Int, + hasDisappointment: Boolean, + savedCount: Int, + isFollowingAuthor: Boolean + ) { + tracker.track( + eventName = "place_map_removed", + properties = """ + { + "review_id" : $reviewId, + "author_user_id" : $authorUserId, + "place_name" : "$placeName", + "menu_count" : $menuCount, + "satisfaction_score" : $satisfactionScore, + "review_length" : $reviewLength, + "photo_count" : $photoCount, + "has_disappointment" : $hasDisappointment, + "saved_count" : $savedCount, + "is_following_author" : $isFollowingAuthor + } + """.trimIndent() + ) + } + + fun directionClicked( + reviewId: Int, + authorUserId: Int, + placeName: String, +// category: String, + menuCount: Int, + satisfactionScore: Double, + reviewLength: Int, + photoCount: Int, + hasDisappointment: Boolean, + savedCount: Int, + isFollowingAuthor: Boolean + ) { + tracker.track( + eventName = "direction_clicked", + properties = """ + { + "review_id" : $reviewId, + "author_user_id" : $authorUserId, + "place_name" : "$placeName", + "menu_count" : $menuCount, + "satisfaction_score" : $satisfactionScore, + "review_length" : $reviewLength, + "photo_count" : $photoCount, + "has_disappointment" : $hasDisappointment, + "saved_count" : $savedCount, + "is_following_author" : $isFollowingAuthor + } + """.trimIndent() + ) + } +} diff --git a/app/src/main/java/com/spoony/spoony/presentation/placeDetail/PlaceDetailRoute.kt b/app/src/main/java/com/spoony/spoony/presentation/placeDetail/PlaceDetailRoute.kt index 92cbcb4cd..20e423b38 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/placeDetail/PlaceDetailRoute.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/placeDetail/PlaceDetailRoute.kt @@ -160,6 +160,18 @@ fun PlaceDetailRoute( onClickPositive = { viewModel.useSpoon(postId) scoopDialogVisibility = false + tracker.reviewDetailEvents.spoonUsed( + reviewId = postId, + authorUserId = userProfile.userId, + placeName = uiState.data.placeName, + menuCount = uiState.data.menuList.size, + satisfactionScore = uiState.data.value, + reviewLength = uiState.data.description.length, + photoCount = uiState.data.photoUrlList.size, + hasDisappointment = uiState.data.cons.isNotEmpty(), + savedCount = state.addMapCount, + isFollowingAuthor = state.isFollowing + ) }, onClickNegative = { scoopDialogVisibility = false @@ -207,6 +219,18 @@ fun PlaceDetailRoute( addMapCount = state.addMapCount, isAddMap = state.isAddMap, onSearchMapClick = { + tracker.reviewDetailEvents.directionClicked( + reviewId = postId, + authorUserId = userProfile.userId, + placeName = uiState.data.placeName, + menuCount = uiState.data.menuList.size, + satisfactionScore = uiState.data.value, + reviewLength = uiState.data.description.length, + photoCount = uiState.data.photoUrlList.size, + hasDisappointment = uiState.data.cons.isNotEmpty(), + savedCount = state.addMapCount, + isFollowingAuthor = state.isFollowing + ) searchPlaceNaverMap( latitude = data.latitude, longitude = data.longitude, @@ -215,8 +239,36 @@ fun PlaceDetailRoute( ) }, isNotMine = !data.isMine, - onAddMapButtonClick = { viewModel.addMyMap(postId) }, - onDeletePinMapButtonClick = { viewModel.deletePinMap(postId) } + onAddMapButtonClick = { + viewModel.addMyMap(postId) + tracker.reviewDetailEvents.placeMapSaved( + reviewId = postId, + authorUserId = userProfile.userId, + placeName = uiState.data.placeName, + menuCount = uiState.data.menuList.size, + satisfactionScore = uiState.data.value, + reviewLength = uiState.data.description.length, + photoCount = uiState.data.photoUrlList.size, + hasDisappointment = uiState.data.cons.isNotEmpty(), + savedCount = state.addMapCount, + isFollowingAuthor = state.isFollowing + ) + }, + onDeletePinMapButtonClick = { + viewModel.deletePinMap(postId) + tracker.reviewDetailEvents.placeMapRemoved( + reviewId = postId, + authorUserId = userProfile.userId, + placeName = uiState.data.placeName, + menuCount = uiState.data.menuList.size, + satisfactionScore = uiState.data.value, + reviewLength = uiState.data.description.length, + photoCount = uiState.data.photoUrlList.size, + hasDisappointment = uiState.data.cons.isNotEmpty(), + savedCount = state.addMapCount, + isFollowingAuthor = state.isFollowing + ) + } ) }, content = { paddingValues -> @@ -282,7 +334,24 @@ fun PlaceDetailRoute( isScooped = state.isScooped || data.isMine, dropdownMenuList = dropDownMenuList, onReportButtonClick = { navigateToReport(postId, ReportType.POST) }, - onShowSnackBar = viewModel::showSnackBar + onShowSnackBar = viewModel::showSnackBar, + trackSpoonUseIntent = { + tracker.reviewDetailEvents.spoonUseIntent( + reviewId = postId, + authorUserId = userProfile.userId, + placeName = uiState.data.placeName, + menuCount = uiState.data.menuList.size, + satisfactionScore = uiState.data.value, + reviewLength = uiState.data.description.length, + photoCount = uiState.data.photoUrlList.size, + hasDisappointment = uiState.data.cons.isNotEmpty(), + savedCount = state.addMapCount, + isFollowingAuthor = state.isFollowing + ) + }, + trackSpoonUseFailed = { + tracker.reviewDetailEvents.spoonUseFailed() + } ) } ) @@ -316,7 +385,9 @@ private fun PlaceDetailScreen( isScooped: Boolean, dropdownMenuList: ImmutableList, onReportButtonClick: () -> Unit, - onShowSnackBar: (String) -> Unit + onShowSnackBar: (String) -> Unit, + trackSpoonUseIntent: () -> Unit, + trackSpoonUseFailed: () -> Unit ) { val scrollState = rememberScrollState() Column( @@ -412,8 +483,10 @@ private fun PlaceDetailScreen( onScoopButtonClick = { if (spoonAmount > 0) { onScoopButtonClick() + trackSpoonUseIntent() } else { onShowSnackBar("남은 스푼이 없어요 ㅠ.ㅠ") + trackSpoonUseFailed() } }, isBlurred = !isScooped From 80d50b25cd13f2c89ed71f9d15e304733bc702e5 Mon Sep 17 00:00:00 2001 From: Hyobeen-Park Date: Sun, 5 Oct 2025 03:04:44 +0900 Subject: [PATCH 22/31] [CHORE/#386] mixpanel version upgrade --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3c5303790..073eb4442 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -70,7 +70,7 @@ playServicesMaps = "19.2.0" room = "2.8.1" ## mixpanel -mixpanel = "7.+" +mixpanel = "8.2.4" [libraries] # Test From c6de6e676a735c0a2c9338f83abf773fd7bcf93a Mon Sep 17 00:00:00 2001 From: Hyobeen-Park Date: Sun, 5 Oct 2025 03:07:24 +0900 Subject: [PATCH 23/31] [CHORE/#386] update pr-checker --- .github/workflows/pr_checker.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/pr_checker.yml b/.github/workflows/pr_checker.yml index 0d423bbfc..695a65395 100644 --- a/.github/workflows/pr_checker.yml +++ b/.github/workflows/pr_checker.yml @@ -47,6 +47,7 @@ jobs: RELEASE_KEY_PASSWORD: ${{ secrets.RELEASE_KEY_PASSWORD }} RELEASE_STORE_PASSWORD: ${{ secrets.RELEASE_STORE_PASSWORD }} NATIVE_APP_KEY: ${{ secrets.NATIVE_APP_KEY }} + MIXPANEL_KEY: ${{ secrets.MIXPANEL_KEY }} run: | echo prod.base.url=\"$PROD_BASE_URL\" >> local.properties echo dev.base.url=\"$DEV_BASE_URL\" >> local.properties @@ -57,6 +58,8 @@ jobs: echo storePassword=$RELEASE_STORE_PASSWORD >> local.properties echo native.app.key=\"$NATIVE_APP_KEY\" >> local.properties echo nativeAppKey=$NATIVE_APP_KEY >> local.properties + echo mixpanelDevKey=\"$MIXPANEL_KEY\" >> local.properties + echo mixpanelProdKey=\"$MIXPANEL_KEY\" >> local.properties - name: Lint Check run: ./gradlew ktlintCheck -PcompileSdkVersion=35 From ae0ad498b3fad9503bf39496bf6a6aebf9d829d5 Mon Sep 17 00:00:00 2001 From: Hyobeen-Park Date: Mon, 6 Oct 2025 04:35:53 +0900 Subject: [PATCH 24/31] =?UTF-8?q?[MOD/#386]=20explore=20events=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 --- .../presentation/explore/ExploreScreen.kt | 2 +- .../explore/component/ExploreFilterSection.kt | 17 +++++++++++++++-- .../exploreSearch/ExploreSearchScreen.kt | 7 ++++++- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/spoony/spoony/presentation/explore/ExploreScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/explore/ExploreScreen.kt index 3bd8ee2a6..7f0dfd3e8 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/explore/ExploreScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/explore/ExploreScreen.kt @@ -111,7 +111,7 @@ fun ExploreRoute( viewModel.refresh() } } - // TODO: + with(state) { ExploreScreen( paddingValues = paddingValues, diff --git a/app/src/main/java/com/spoony/spoony/presentation/explore/component/ExploreFilterSection.kt b/app/src/main/java/com/spoony/spoony/presentation/explore/component/ExploreFilterSection.kt index 7bbf9ba10..a3966cd61 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/explore/component/ExploreFilterSection.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/explore/component/ExploreFilterSection.kt @@ -6,6 +6,7 @@ import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import com.spoony.spoony.core.analytics.events.LocalTracker import com.spoony.spoony.presentation.explore.ExploreAction import com.spoony.spoony.presentation.explore.ExploreFilterItems import com.spoony.spoony.presentation.explore.ExploreFilterState @@ -30,6 +31,8 @@ fun ExploreFilterSection( filterItems: ExploreFilterItems, onAction: (ExploreAction) -> Unit ) { + val tracker = LocalTracker.current + var isSortingBottomSheetVisible by remember { mutableStateOf(false) } var isFilterBottomSheetVisible by remember { mutableStateOf(false) } var exploreFilterBottomSheetTabIndex by remember { mutableIntStateOf(0) } @@ -48,7 +51,14 @@ fun ExploreFilterSection( onFilterClick = { filterType -> handleFilterClick( filterType = filterType, - onLocalReviewButtonClick = { onAction(ExploreAction.ClickLocalReview) }, + onLocalReviewButtonClick = { + tracker.commonEvents.filterApplied( + pageApplied = "explore", + localReviewFilter = !(selectedFilterState.properties[2] ?: false) + ) + + onAction(ExploreAction.ClickLocalReview) + }, updateBottomSheetState = { index, isVisible -> exploreFilterBottomSheetTabIndex = index isFilterBottomSheetVisible = isVisible @@ -62,7 +72,10 @@ fun ExploreFilterSection( if (isSortingBottomSheetVisible) { ExploreSortingBottomSheet( onDismiss = { isSortingBottomSheetVisible = false }, - onClick = { onAction(ExploreAction.ChangeSorting(it)) }, + onClick = { sortType -> + onAction(ExploreAction.ChangeSorting(sortType)) + tracker.exploreEvents.sortSelected(sortType.trackingCode) + }, currentSortingOption = selectedSortingOption ) } diff --git a/app/src/main/java/com/spoony/spoony/presentation/exploreSearch/ExploreSearchScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/exploreSearch/ExploreSearchScreen.kt index 38189bc44..de09b121b 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/exploreSearch/ExploreSearchScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/exploreSearch/ExploreSearchScreen.kt @@ -76,7 +76,6 @@ fun ExploreSearchRoute( val state by viewModel.state.collectAsStateWithLifecycle() val lifecycleOwner = LocalLifecycleOwner.current val showSnackBar = LocalSnackBarTrigger.current - val tracker = LocalTracker.current LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { viewModel.sideEffect.flowWithLifecycle(lifecycleOwner.lifecycle).collect { effect -> @@ -122,6 +121,8 @@ private fun ExploreSearchScreen( placeReviewInfoList: UiState>, onAction: (ExploreSearchAction) -> Unit ) { + val tracker = LocalTracker.current + val focusRequester = remember { FocusRequester() } val focusManager = LocalFocusManager.current var tabRowIndex by rememberSaveable { mutableIntStateOf(0) } @@ -169,6 +170,10 @@ private fun ExploreSearchScreen( onBackButtonClick = { onAction(ExploreSearchAction.ClickBack) }, onSearchAction = { onAction(ExploreSearchAction.Search(searchText)) + tracker.exploreEvents.exploreSearched( + searchTargetType = searchType.trackingCode, + searchTerm = searchText + ) }, focusRequester = focusRequester, searchType = searchType From 1ba8be0a3f1d3e2aa2150eed83d1966ef56b4459 Mon Sep 17 00:00:00 2001 From: Hyobeen-Park Date: Mon, 6 Oct 2025 04:48:05 +0900 Subject: [PATCH 25/31] =?UTF-8?q?[MOD/#386]=20category=20=EC=86=8D?= =?UTF-8?q?=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 --- .../spoony/core/analytics/events/CommonEvents.kt | 9 ++++++--- .../core/analytics/events/ReviewDetailEvents.kt | 15 ++++++++++----- .../presentation/placeDetail/PlaceDetailRoute.kt | 8 ++++++++ .../placeDetail/model/PlaceDetailModel.kt | 8 ++++++-- 4 files changed, 30 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/events/CommonEvents.kt b/app/src/main/java/com/spoony/spoony/core/analytics/events/CommonEvents.kt index 1142eea81..0421da6f7 100644 --- a/app/src/main/java/com/spoony/spoony/core/analytics/events/CommonEvents.kt +++ b/app/src/main/java/com/spoony/spoony/core/analytics/events/CommonEvents.kt @@ -22,7 +22,7 @@ class CommonEvents @Inject constructor( reviewId: Int, authorUserId: Int, placeName: String, -// category: String, + category: String, menuCount: Int, satisfactionScore: Double, reviewLength: Int, @@ -41,6 +41,7 @@ class CommonEvents @Inject constructor( "review_id": $reviewId, "author_user_id": $authorUserId, "place_name": "$placeName", + "category" : "$category", "menu_count": $menuCount, "satisfaction_score": $satisfactionScore, "review_length": $reviewLength, @@ -137,7 +138,7 @@ class CommonEvents @Inject constructor( reviewId: Int, authorUserId: Int, placeName: String, -// category: String, + category: String, menuCount: Int, satisfactionScore: Double, reviewLength: Int, @@ -152,6 +153,7 @@ class CommonEvents @Inject constructor( "review_id": $reviewId, "author_user_id": $authorUserId, "place_name": "$placeName", + "category": "$category", "menu_count": $menuCount, "satisfaction_score": $satisfactionScore, "review_length": $reviewLength, @@ -168,7 +170,7 @@ class CommonEvents @Inject constructor( reviewId: Int, authorUserId: Int, placeName: String, -// category: String, + category: String, menuCount: Int, satisfactionScore: Double, reviewLength: Int, @@ -183,6 +185,7 @@ class CommonEvents @Inject constructor( "review_id": $reviewId, "author_user_id": $authorUserId, "place_name": "$placeName", + "category": "$category", "menu_count": $menuCount, "satisfaction_score": $satisfactionScore, "review_length": $reviewLength, diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/events/ReviewDetailEvents.kt b/app/src/main/java/com/spoony/spoony/core/analytics/events/ReviewDetailEvents.kt index 19654b8ce..a650cce1f 100644 --- a/app/src/main/java/com/spoony/spoony/core/analytics/events/ReviewDetailEvents.kt +++ b/app/src/main/java/com/spoony/spoony/core/analytics/events/ReviewDetailEvents.kt @@ -10,7 +10,7 @@ class ReviewDetailEvents @Inject constructor( reviewId: Int, authorUserId: Int, placeName: String, -// category: String, + category: String, menuCount: Int, satisfactionScore: Double, reviewLength: Int, @@ -26,6 +26,7 @@ class ReviewDetailEvents @Inject constructor( "review_id" : $reviewId, "author_user_id" : $authorUserId, "place_name" : "$placeName", + "category" : "$category", "menu_count" : $menuCount, "satisfaction_score" : $satisfactionScore, "review_length" : $reviewLength, @@ -42,7 +43,7 @@ class ReviewDetailEvents @Inject constructor( reviewId: Int, authorUserId: Int, placeName: String, -// category: String, + category: String, menuCount: Int, satisfactionScore: Double, reviewLength: Int, @@ -58,6 +59,7 @@ class ReviewDetailEvents @Inject constructor( "review_id" : $reviewId, "author_user_id" : $authorUserId, "place_name" : "$placeName", + "category": "$category", "menu_count" : $menuCount, "satisfaction_score" : $satisfactionScore, "review_length" : $reviewLength, @@ -78,7 +80,7 @@ class ReviewDetailEvents @Inject constructor( reviewId: Int, authorUserId: Int, placeName: String, -// category: String, + category: String, menuCount: Int, satisfactionScore: Double, reviewLength: Int, @@ -94,6 +96,7 @@ class ReviewDetailEvents @Inject constructor( "review_id" : $reviewId, "author_user_id" : $authorUserId, "place_name" : "$placeName", + "category" : "$category", "menu_count" : $menuCount, "satisfaction_score" : $satisfactionScore, "review_length" : $reviewLength, @@ -110,7 +113,7 @@ class ReviewDetailEvents @Inject constructor( reviewId: Int, authorUserId: Int, placeName: String, -// category: String, + category: String, menuCount: Int, satisfactionScore: Double, reviewLength: Int, @@ -126,6 +129,7 @@ class ReviewDetailEvents @Inject constructor( "review_id" : $reviewId, "author_user_id" : $authorUserId, "place_name" : "$placeName", + "category": "$category", "menu_count" : $menuCount, "satisfaction_score" : $satisfactionScore, "review_length" : $reviewLength, @@ -142,7 +146,7 @@ class ReviewDetailEvents @Inject constructor( reviewId: Int, authorUserId: Int, placeName: String, -// category: String, + category: String, menuCount: Int, satisfactionScore: Double, reviewLength: Int, @@ -158,6 +162,7 @@ class ReviewDetailEvents @Inject constructor( "review_id" : $reviewId, "author_user_id" : $authorUserId, "place_name" : "$placeName", + "category": "$category", "menu_count" : $menuCount, "satisfaction_score" : $satisfactionScore, "review_length" : $reviewLength, diff --git a/app/src/main/java/com/spoony/spoony/presentation/placeDetail/PlaceDetailRoute.kt b/app/src/main/java/com/spoony/spoony/presentation/placeDetail/PlaceDetailRoute.kt index 20e423b38..39310725b 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/placeDetail/PlaceDetailRoute.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/placeDetail/PlaceDetailRoute.kt @@ -144,6 +144,7 @@ fun PlaceDetailRoute( reviewId = postId, authorUserId = userProfile.userId, placeName = uiState.data.placeName, + category = uiState.data.category.categoryName, menuCount = uiState.data.menuList.size, satisfactionScore = uiState.data.value, reviewLength = uiState.data.description.length, @@ -164,6 +165,7 @@ fun PlaceDetailRoute( reviewId = postId, authorUserId = userProfile.userId, placeName = uiState.data.placeName, + category = uiState.data.category.categoryName, menuCount = uiState.data.menuList.size, satisfactionScore = uiState.data.value, reviewLength = uiState.data.description.length, @@ -223,6 +225,7 @@ fun PlaceDetailRoute( reviewId = postId, authorUserId = userProfile.userId, placeName = uiState.data.placeName, + category = uiState.data.category.categoryName, menuCount = uiState.data.menuList.size, satisfactionScore = uiState.data.value, reviewLength = uiState.data.description.length, @@ -245,6 +248,7 @@ fun PlaceDetailRoute( reviewId = postId, authorUserId = userProfile.userId, placeName = uiState.data.placeName, + category = uiState.data.category.categoryName, menuCount = uiState.data.menuList.size, satisfactionScore = uiState.data.value, reviewLength = uiState.data.description.length, @@ -260,6 +264,7 @@ fun PlaceDetailRoute( reviewId = postId, authorUserId = userProfile.userId, placeName = uiState.data.placeName, + category = uiState.data.category.categoryName, menuCount = uiState.data.menuList.size, satisfactionScore = uiState.data.value, reviewLength = uiState.data.description.length, @@ -304,6 +309,7 @@ fun PlaceDetailRoute( reviewId = postId, authorUserId = userProfile.userId, placeName = uiState.data.placeName, + category = uiState.data.category.categoryName, menuCount = uiState.data.menuList.size, satisfactionScore = uiState.data.value, reviewLength = uiState.data.description.length, @@ -316,6 +322,7 @@ fun PlaceDetailRoute( reviewId = postId, authorUserId = userProfile.userId, placeName = uiState.data.placeName, + category = uiState.data.category.categoryName, menuCount = uiState.data.menuList.size, satisfactionScore = uiState.data.value, reviewLength = uiState.data.description.length, @@ -340,6 +347,7 @@ fun PlaceDetailRoute( reviewId = postId, authorUserId = userProfile.userId, placeName = uiState.data.placeName, + category = uiState.data.category.categoryName, menuCount = uiState.data.menuList.size, satisfactionScore = uiState.data.value, reviewLength = uiState.data.description.length, diff --git a/app/src/main/java/com/spoony/spoony/presentation/placeDetail/model/PlaceDetailModel.kt b/app/src/main/java/com/spoony/spoony/presentation/placeDetail/model/PlaceDetailModel.kt index 52699ac5e..10d833a8a 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/placeDetail/model/PlaceDetailModel.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/placeDetail/model/PlaceDetailModel.kt @@ -1,6 +1,8 @@ package com.spoony.spoony.presentation.placeDetail.model import com.spoony.spoony.domain.entity.PlaceReviewEntity +import com.spoony.spoony.presentation.gourmet.map.model.CategoryModel +import com.spoony.spoony.presentation.gourmet.map.model.toModel import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -16,7 +18,8 @@ data class PlaceDetailModel( val placeAddress: String, val latitude: Double, val longitude: Double, - val isMine: Boolean + val isMine: Boolean, + val category: CategoryModel ) fun PlaceReviewEntity.toModel(): PlaceDetailModel = PlaceDetailModel( @@ -30,5 +33,6 @@ fun PlaceReviewEntity.toModel(): PlaceDetailModel = PlaceDetailModel( placeAddress = this.placeAddress ?: "", latitude = this.latitude ?: 0.0, longitude = this.longitude ?: 0.0, - isMine = this.isMine ?: false + isMine = this.isMine ?: false, + category = this.category?.toModel() ?: CategoryModel() ) From 0056ac333b82b2e619bc4da0714807e6461f17fb Mon Sep 17 00:00:00 2001 From: Hyobeen-Park Date: Mon, 6 Oct 2025 05:00:03 +0900 Subject: [PATCH 26/31] =?UTF-8?q?[MOD/#386]=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EC=8B=9C=20=EB=B9=A0=EC=A7=84=20=EC=86=8D?= =?UTF-8?q?=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 --- .../spoony/core/analytics/events/CommonEvents.kt | 10 ++++++---- .../spoony/presentation/register/RegisterEndScreen.kt | 5 +++-- .../presentation/register/model/PlaceReviewModel.kt | 10 +++++++--- .../presentation/register/model/RegisterState.kt | 5 ++++- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/events/CommonEvents.kt b/app/src/main/java/com/spoony/spoony/core/analytics/events/CommonEvents.kt index 0421da6f7..d3d858f04 100644 --- a/app/src/main/java/com/spoony/spoony/core/analytics/events/CommonEvents.kt +++ b/app/src/main/java/com/spoony/spoony/core/analytics/events/CommonEvents.kt @@ -58,15 +58,15 @@ class CommonEvents @Inject constructor( fun reviewEdited( reviewId: Int, -// authorUserId: Int, + authorUserId: Int, placeName: String, category: String, menuCount: Int, satisfactionScore: Float, reviewLength: Int, photoCount: Int, - hasDisappointment: Boolean -// savedCount: Int, + hasDisappointment: Boolean, + savedCount: Int // entryPoint: String ) { tracker.track( @@ -74,13 +74,15 @@ class CommonEvents @Inject constructor( properties = """ { "review_id": $reviewId, + "author_user_id": $authorUserId, "place_name": "$placeName", "category": "$category", "menu_count": $menuCount, "satisfaction_score": $satisfactionScore, "review_length": $reviewLength, "photo_count": $photoCount, - "has_disappointment": $hasDisappointment + "has_disappointment": $hasDisappointment, + "saved_count": $savedCount } """.trimIndent() ) diff --git a/app/src/main/java/com/spoony/spoony/presentation/register/RegisterEndScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/register/RegisterEndScreen.kt index a76ab7a44..99eacb52a 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/register/RegisterEndScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/register/RegisterEndScreen.kt @@ -81,16 +81,17 @@ fun RegisterEndRoute( onRegisterComplete = onRegisterComplete, onEditComplete = { postId -> onEditComplete(postId) - tracker.commonEvents.reviewEdited( reviewId = postId, + authorUserId = state.userId, placeName = state.selectedPlace.placeName, category = state.selectedCategory.categoryName, menuCount = state.menuList.size, satisfactionScore = state.userSatisfactionValue, reviewLength = state.detailReview.length, photoCount = state.selectedPhotos.size, - hasDisappointment = state.optionalReview.isNotEmpty() + hasDisappointment = state.optionalReview.isNotEmpty(), + savedCount = state.addMapCount ) }, postId = viewModel.postId, diff --git a/app/src/main/java/com/spoony/spoony/presentation/register/model/PlaceReviewModel.kt b/app/src/main/java/com/spoony/spoony/presentation/register/model/PlaceReviewModel.kt index b103cf4b8..8dcf67205 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/register/model/PlaceReviewModel.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/register/model/PlaceReviewModel.kt @@ -19,7 +19,8 @@ data class PlaceReviewModel( val placeAddress: String, val latitude: Double, val longitude: Double, - val category: CategoryState + val category: CategoryState, + val addMapCount: Int ) fun PlaceReviewEntity.toModel(): PlaceReviewModel = @@ -36,7 +37,8 @@ fun PlaceReviewEntity.toModel(): PlaceReviewModel = placeAddress = placeAddress ?: "", latitude = latitude ?: 0.0, longitude = longitude ?: 0.0, - category = category?.toModel() ?: CategoryState(0, "", "", "") + category = category?.toModel() ?: CategoryState(0, "", "", ""), + addMapCount = addMapCount ?: -1 ) fun PlaceReviewModel.toRegisterState(currentState: RegisterState): RegisterState = @@ -56,5 +58,7 @@ fun PlaceReviewModel.toRegisterState(currentState: RegisterState): RegisterState originalPhotoUrls = photoUrls, selectedPhotos = photoUrls.map { url -> SelectedPhoto(uri = url.toUri(), isFromServer = true) - }.toImmutableList() + }.toImmutableList(), + userId = this.userId, + addMapCount = this.addMapCount ) diff --git a/app/src/main/java/com/spoony/spoony/presentation/register/model/RegisterState.kt b/app/src/main/java/com/spoony/spoony/presentation/register/model/RegisterState.kt index 6f43d33c0..f2230ab68 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/register/model/RegisterState.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/register/model/RegisterState.kt @@ -24,7 +24,10 @@ data class RegisterState( val currentStep: Float = 1f, val isLoading: Boolean = false, val isSubmitting: Boolean = false, - val error: String? = null + val error: String? = null, + + val userId: Int = -1, + val addMapCount: Int = -1 ) { companion object { const val DEFAULT = 50f From a54d41b2ae69d7b4a5fef6b4ea71e13dc075373c Mon Sep 17 00:00:00 2001 From: Hyobeen-Park Date: Wed, 8 Oct 2025 06:02:33 +0900 Subject: [PATCH 27/31] =?UTF-8?q?[FEAT/#386]=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83,=20=ED=9A=8C=EC=9B=90=ED=83=88=ED=87=B4=20=EC=8B=9C?= =?UTF-8?q?=20userProfile=20=EC=B4=88=EA=B8=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/spoony/spoony/core/analytics/MixPanelTracker.kt | 9 +++++++++ .../core/analytics/events/MixPanelUserProperties.kt | 4 ++++ .../presentation/setting/account/AccountDeleteScreen.kt | 3 +++ .../setting/account/AccountManagementScreen.kt | 4 +++- 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt b/app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt index 4da05b162..687589a7b 100644 --- a/app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt +++ b/app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt @@ -24,6 +24,10 @@ class MixPanelTracker @Inject constructor( } } + fun resetUserProfile() { + mixpanel.reset() + } + fun track(eventName: String) { Timber.tag("mixpanel").d(eventName) mixpanel.track(eventName) @@ -34,6 +38,11 @@ class MixPanelTracker @Inject constructor( mixpanel.track(eventName, properties.toJsonObject()) } + fun track(eventName: String, properties: JSONObject) { + Timber.tag("mixpanel").d("$eventName $properties") + mixpanel.track(eventName, properties) + } + private fun String.toJsonObject(): JSONObject { return try { JSONObject(this) diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelUserProperties.kt b/app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelUserProperties.kt index 9603947fc..24e28069d 100644 --- a/app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelUserProperties.kt +++ b/app/src/main/java/com/spoony/spoony/core/analytics/events/MixPanelUserProperties.kt @@ -9,4 +9,8 @@ class MixPanelUserProperties @Inject constructor( fun setUserProfile(userId: String, properties: Map) { tracker.setUserProfile(userId = userId, properties = properties) } + + fun resetUserProfile() { + tracker.resetUserProfile() + } } diff --git a/app/src/main/java/com/spoony/spoony/presentation/setting/account/AccountDeleteScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/setting/account/AccountDeleteScreen.kt index 7f9fa31d5..1109f15d4 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/setting/account/AccountDeleteScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/setting/account/AccountDeleteScreen.kt @@ -29,6 +29,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.flowWithLifecycle import com.jakewharton.processphoenix.ProcessPhoenix +import com.spoony.spoony.core.analytics.events.LocalTracker import com.spoony.spoony.core.designsystem.component.button.SpoonyButton import com.spoony.spoony.core.designsystem.component.topappbar.TitleTopAppBar import com.spoony.spoony.core.designsystem.theme.SpoonyAndroidTheme @@ -46,9 +47,11 @@ fun AccountDeleteScreen( ) { val context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current + val tracker = LocalTracker.current LaunchedEffect(viewModel.restartTrigger, lifecycleOwner) { viewModel.restartTrigger.flowWithLifecycle(lifecycleOwner.lifecycle).collect { effect -> + tracker.userProperties.resetUserProfile() ProcessPhoenix.triggerRebirth(context) } } diff --git a/app/src/main/java/com/spoony/spoony/presentation/setting/account/AccountManagementScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/setting/account/AccountManagementScreen.kt index 16ec04161..45ca3a464 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/setting/account/AccountManagementScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/setting/account/AccountManagementScreen.kt @@ -23,8 +23,8 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.flowWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.jakewharton.processphoenix.ProcessPhoenix +import com.spoony.spoony.core.analytics.events.LocalTracker import com.spoony.spoony.core.designsystem.component.dialog.TwoButtonDialog import com.spoony.spoony.core.designsystem.component.topappbar.TitleTopAppBar import com.spoony.spoony.core.designsystem.theme.SpoonyAndroidTheme @@ -40,9 +40,11 @@ internal fun AccountManagementScreen( ) { val context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current + val tracker = LocalTracker.current LaunchedEffect(viewModel.restartTrigger, lifecycleOwner) { viewModel.restartTrigger.flowWithLifecycle(lifecycleOwner.lifecycle).collect { effect -> + tracker.userProperties.resetUserProfile() ProcessPhoenix.triggerRebirth(context) } } From 71131faae8f298a056d62a508e21d0fa7513ad5f Mon Sep 17 00:00:00 2001 From: Hyobeen-Park Date: Wed, 8 Oct 2025 06:15:01 +0900 Subject: [PATCH 28/31] =?UTF-8?q?[MOD/#386]=20place=5Fviewed=20=ED=8A=B8?= =?UTF-8?q?=EB=9E=98=ED=82=B9=20=EC=9C=84=EC=B9=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../placeDetail/PlaceDetailRoute.kt | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/spoony/spoony/presentation/placeDetail/PlaceDetailRoute.kt b/app/src/main/java/com/spoony/spoony/presentation/placeDetail/PlaceDetailRoute.kt index 39310725b..8bec7e0d3 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/placeDetail/PlaceDetailRoute.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/placeDetail/PlaceDetailRoute.kt @@ -133,15 +133,14 @@ fun PlaceDetailRoute( ) } - when (val uiState = state.placeDetailModel) { - is UiState.Empty -> {} - is UiState.Loading -> {} - is UiState.Failure -> {} - is UiState.Success -> { - val postId = (state.reviewId as? UiState.Success)?.data ?: return + LaunchedEffect(state.reviewId, userProfile.userId) { + if (state.reviewId !is UiState.Success) return@LaunchedEffect + if (userProfile.userId == -1) return@LaunchedEffect + val uiState = state.placeDetailModel + if (uiState is UiState.Success) { tracker.commonEvents.reviewViewed( - reviewId = postId, + reviewId = (state.reviewId as UiState.Success).data, authorUserId = userProfile.userId, placeName = uiState.data.placeName, category = uiState.data.category.categoryName, @@ -155,6 +154,15 @@ fun PlaceDetailRoute( isFollowedUserReview = state.isFollowing, isSavedReview = state.isAddMap ) + } + } + + when (val uiState = state.placeDetailModel) { + is UiState.Empty -> {} + is UiState.Loading -> {} + is UiState.Failure -> {} + is UiState.Success -> { + val postId = (state.reviewId as? UiState.Success)?.data ?: return if (scoopDialogVisibility) { ScoopDialog( From a27aefd5828a776b7bcf28b7562dbf5a173ddc6d Mon Sep 17 00:00:00 2001 From: Hyobeen-Park Date: Wed, 8 Oct 2025 06:39:19 +0900 Subject: [PATCH 29/31] =?UTF-8?q?[REFACTOR/#386]=20raw=20string=20->=20jso?= =?UTF-8?q?nObject=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/analytics/events/AnalyticsEvents.kt | 9 +- .../core/analytics/events/CommonEvents.kt | 171 ++++++++---------- .../core/analytics/events/ExploreEvents.kt | 19 +- .../spoony/core/analytics/events/MapEvents.kt | 11 +- .../core/analytics/events/MypageEvents.kt | 9 +- .../core/analytics/events/OnboardingEvents.kt | 19 +- .../core/analytics/events/RegisterEvents.kt | 25 ++- .../analytics/events/ReviewDetailEvents.kt | 141 +++++++-------- .../core/analytics/events/SpoonDrawEvents.kt | 9 +- 9 files changed, 187 insertions(+), 226 deletions(-) diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/events/AnalyticsEvents.kt b/app/src/main/java/com/spoony/spoony/core/analytics/events/AnalyticsEvents.kt index e2ee57853..230e122d2 100644 --- a/app/src/main/java/com/spoony/spoony/core/analytics/events/AnalyticsEvents.kt +++ b/app/src/main/java/com/spoony/spoony/core/analytics/events/AnalyticsEvents.kt @@ -2,6 +2,7 @@ package com.spoony.spoony.core.analytics.events import com.spoony.spoony.core.analytics.MixPanelTracker import jakarta.inject.Inject +import org.json.JSONObject class AnalyticsEvents @Inject constructor( private val tracker: MixPanelTracker @@ -13,11 +14,9 @@ class AnalyticsEvents @Inject constructor( fun signupCompleted(signupMethod: String) { tracker.track( eventName = "signup_completed", - properties = """ - { - "signup_method": "$signupMethod" - } - """.trimIndent() + properties = JSONObject().apply { + put("signup_method", signupMethod) + } ) } diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/events/CommonEvents.kt b/app/src/main/java/com/spoony/spoony/core/analytics/events/CommonEvents.kt index d3d858f04..d1cd7fc46 100644 --- a/app/src/main/java/com/spoony/spoony/core/analytics/events/CommonEvents.kt +++ b/app/src/main/java/com/spoony/spoony/core/analytics/events/CommonEvents.kt @@ -3,6 +3,7 @@ package com.spoony.spoony.core.analytics.events import com.spoony.spoony.core.analytics.MixPanelTracker import jakarta.inject.Inject import org.json.JSONArray +import org.json.JSONObject class CommonEvents @Inject constructor( private val tracker: MixPanelTracker @@ -10,11 +11,9 @@ class CommonEvents @Inject constructor( fun tabEntered(tabName: String) { tracker.track( eventName = "tab_entered", - properties = """ - { - "tab_name": "$tabName" - } - """.trimIndent() + properties = JSONObject().apply { + put("tab_name", tabName) + } ) } @@ -36,23 +35,21 @@ class CommonEvents @Inject constructor( ) { tracker.track( eventName = "review_viewed", - properties = """ - { - "review_id": $reviewId, - "author_user_id": $authorUserId, - "place_name": "$placeName", - "category" : "$category", - "menu_count": $menuCount, - "satisfaction_score": $satisfactionScore, - "review_length": $reviewLength, - "photo_count": $photoCount, - "has_disappointment": $hasDisappointment, - "saved_count": $savedCount, - "is_self_review": $isSelfReview, - "is_followed_user_review": $isFollowedUserReview, - "is_saved_review": $isSavedReview - } - """.trimIndent() + properties = JSONObject().apply { + put("review_id", reviewId) + put("author_user_id", authorUserId) + put("place_name", placeName) + put("category", category) + put("menu_count", menuCount) + put("satisfaction_score", satisfactionScore) + put("review_length", reviewLength) + put("photo_count", photoCount) + put("has_disappointment", hasDisappointment) + put("saved_count", savedCount) + put("is_self_review", isSelfReview) + put("is_followed_user_review", isFollowedUserReview) + put("is_saved_review", isSavedReview) + } ) } @@ -71,20 +68,18 @@ class CommonEvents @Inject constructor( ) { tracker.track( eventName = "review_edited", - properties = """ - { - "review_id": $reviewId, - "author_user_id": $authorUserId, - "place_name": "$placeName", - "category": "$category", - "menu_count": $menuCount, - "satisfaction_score": $satisfactionScore, - "review_length": $reviewLength, - "photo_count": $photoCount, - "has_disappointment": $hasDisappointment, - "saved_count": $savedCount - } - """.trimIndent() + properties = JSONObject().apply { + put("review_id", reviewId) + put("author_user_id", authorUserId) + put("place_name", placeName) + put("category", category) + put("menu_count", menuCount) + put("satisfaction_score", satisfactionScore) + put("review_length", reviewLength) + put("photo_count", photoCount) + put("has_disappointment", hasDisappointment) + put("saved_count", savedCount) + } ) } @@ -96,13 +91,11 @@ class CommonEvents @Inject constructor( ) { tracker.track( eventName = "profile_viewed", - properties = """ - { - "profile_user_id": $profileUserId, - "is_self_profile": $isSelfProfile, - "is_following_profile_user": $isFollowingProfileUser - } - """.trimIndent() + properties = JSONObject().apply { + put("profile_user_id", profileUserId) + put("is_self_profile", isSelfProfile) + put("is_following_profile_user", isFollowingProfileUser) + } ) } @@ -112,12 +105,10 @@ class CommonEvents @Inject constructor( ) { tracker.track( eventName = "follow_user", - properties = """ - { - "followed_user_id": $followedUserId, - "entry_point" : "$entryPoint" - } - """.trimIndent() + properties = JSONObject().apply { + put("followed_user_id", followedUserId) + put("entry_point", entryPoint) + } ) } @@ -127,12 +118,10 @@ class CommonEvents @Inject constructor( ) { tracker.track( eventName = "unfollow_user", - properties = """ - { - "unfollowed_user_id": $unfollowedUserId, - "entry_point": "$entryPoint" - } - """.trimIndent() + properties = JSONObject().apply { + put("unfollowed_user_id", unfollowedUserId) + put("entry_point", entryPoint) + } ) } @@ -150,21 +139,19 @@ class CommonEvents @Inject constructor( ) { tracker.track( eventName = "follow_user_from_review", - properties = """ - { - "review_id": $reviewId, - "author_user_id": $authorUserId, - "place_name": "$placeName", - "category": "$category", - "menu_count": $menuCount, - "satisfaction_score": $satisfactionScore, - "review_length": $reviewLength, - "photo_count": $photoCount, - "has_disappointment": $hasDisappointment, - "saved_count": $savedCount, - "entry_point" : "review" - } - """.trimIndent() + properties = JSONObject().apply { + put("review_id", reviewId) + put("author_user_id", authorUserId) + put("place_name", placeName) + put("category", category) + put("menu_count", menuCount) + put("satisfaction_score", satisfactionScore) + put("review_length", reviewLength) + put("photo_count", photoCount) + put("has_disappointment", hasDisappointment) + put("saved_count", savedCount) + put("entry_point", "review") + } ) } @@ -182,21 +169,19 @@ class CommonEvents @Inject constructor( ) { tracker.track( eventName = "unfollow_user_from_review", - properties = """ - { - "review_id": $reviewId, - "author_user_id": $authorUserId, - "place_name": "$placeName", - "category": "$category", - "menu_count": $menuCount, - "satisfaction_score": $satisfactionScore, - "review_length": $reviewLength, - "photo_count": $photoCount, - "has_disappointment": $hasDisappointment, - "saved_count": $savedCount, - "entry_point" : "review" - } - """.trimIndent() + properties = JSONObject().apply { + put("review_id", reviewId) + put("author_user_id", authorUserId) + put("place_name", placeName) + put("category", category) + put("menu_count", menuCount) + put("satisfaction_score", satisfactionScore) + put("review_length", reviewLength) + put("photo_count", photoCount) + put("has_disappointment", hasDisappointment) + put("saved_count", savedCount) + put("entry_point", "review") + } ) } @@ -209,15 +194,13 @@ class CommonEvents @Inject constructor( ) { tracker.track( eventName = "filter_applied", - properties = """ - { - "page_applied": "$pageApplied", - "local_review_filter": $localReviewFilter, - "region_filters": ${JSONArray(regionFilters)}, - "category_filters": ${JSONArray(categoryFilters)}, - "age_group_filters": ${JSONArray(ageGroupFilters)} - } - """.trimIndent() + properties = JSONObject().apply { + put("page_applied", pageApplied) + put("local_review_filter", localReviewFilter) + put("region_filters", JSONArray(regionFilters)) + put("category_filters", JSONArray(categoryFilters)) + put("age_group_filters", JSONArray(ageGroupFilters)) + } ) } } diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/events/ExploreEvents.kt b/app/src/main/java/com/spoony/spoony/core/analytics/events/ExploreEvents.kt index 90edc06b4..eb83e1f23 100644 --- a/app/src/main/java/com/spoony/spoony/core/analytics/events/ExploreEvents.kt +++ b/app/src/main/java/com/spoony/spoony/core/analytics/events/ExploreEvents.kt @@ -2,6 +2,7 @@ package com.spoony.spoony.core.analytics.events import com.spoony.spoony.core.analytics.MixPanelTracker import jakarta.inject.Inject +import org.json.JSONObject class ExploreEvents @Inject constructor( private val tracker: MixPanelTracker @@ -9,11 +10,9 @@ class ExploreEvents @Inject constructor( fun sortSelected(sortType: String) { tracker.track( eventName = "sort_selected", - properties = """ - { - "sort_type": "$sortType" - } - """.trimIndent() + properties = JSONObject().apply { + put("sort_type", sortType) + } ) } @@ -23,12 +22,10 @@ class ExploreEvents @Inject constructor( ) { tracker.track( eventName = "explore_searched", - properties = """ - { - "search_target_type": "$searchTargetType", - "search_term": "$searchTerm" - } - """.trimIndent() + properties = JSONObject().apply { + put("search_target_type", searchTargetType) + put("search_term", searchTerm) + } ) } } diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/events/MapEvents.kt b/app/src/main/java/com/spoony/spoony/core/analytics/events/MapEvents.kt index 84b9154b6..ff9b0d91e 100644 --- a/app/src/main/java/com/spoony/spoony/core/analytics/events/MapEvents.kt +++ b/app/src/main/java/com/spoony/spoony/core/analytics/events/MapEvents.kt @@ -2,6 +2,7 @@ package com.spoony.spoony.core.analytics.events import com.spoony.spoony.core.analytics.MixPanelTracker import jakarta.inject.Inject +import org.json.JSONObject class MapEvents @Inject constructor( private val tracker: MixPanelTracker @@ -12,12 +13,10 @@ class MapEvents @Inject constructor( ) { tracker.track( eventName = "map_searched", - properties = """ - { - "location_type": "$locationType", - "search_term": "$searchTerm" - } - """.trimIndent() + properties = JSONObject().apply { + put("location_type", locationType) + put("search_term", searchTerm) + } ) } } diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/events/MypageEvents.kt b/app/src/main/java/com/spoony/spoony/core/analytics/events/MypageEvents.kt index 1f1721ac9..93b7c54fc 100644 --- a/app/src/main/java/com/spoony/spoony/core/analytics/events/MypageEvents.kt +++ b/app/src/main/java/com/spoony/spoony/core/analytics/events/MypageEvents.kt @@ -3,6 +3,7 @@ package com.spoony.spoony.core.analytics.events import com.spoony.spoony.core.analytics.MixPanelTracker import jakarta.inject.Inject import org.json.JSONArray +import org.json.JSONObject class MypageEvents @Inject constructor( private val tracker: MixPanelTracker @@ -10,11 +11,9 @@ class MypageEvents @Inject constructor( fun profileUpdated(fieldsUpdated: List = listOf()) { tracker.track( eventName = "profile_updated", - properties = """ - { - "fields_updated": ${JSONArray(fieldsUpdated)} - } - """.trimIndent() + properties = JSONObject().apply { + put("fields_updated", JSONArray(fieldsUpdated)) + } ) } diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/events/OnboardingEvents.kt b/app/src/main/java/com/spoony/spoony/core/analytics/events/OnboardingEvents.kt index 30271459a..483c849ea 100644 --- a/app/src/main/java/com/spoony/spoony/core/analytics/events/OnboardingEvents.kt +++ b/app/src/main/java/com/spoony/spoony/core/analytics/events/OnboardingEvents.kt @@ -2,6 +2,7 @@ package com.spoony.spoony.core.analytics.events import com.spoony.spoony.core.analytics.MixPanelTracker import jakarta.inject.Inject +import org.json.JSONObject class OnboardingEvents @Inject constructor( private val tracker: MixPanelTracker @@ -16,12 +17,10 @@ class OnboardingEvents @Inject constructor( ) { tracker.track( eventName = "onboard_2_completed", - properties = """ - { - "birthdate_entered": $isBirthdateEntered, - "active_region_entered": $isActiveRegionEntered - } - """.trimIndent() + properties = JSONObject().apply { + put("birthdate_entered", isBirthdateEntered) + put("active_region_entered", isActiveRegionEntered) + } ) } @@ -32,11 +31,9 @@ class OnboardingEvents @Inject constructor( fun onboard3Completed(bioLength: Int) { tracker.track( eventName = "onboard_3_completed", - properties = """ - { - "bio_length": $bioLength - } - """.trimIndent() + properties = JSONObject().apply { + put("bio_length", bioLength) + } ) } diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/events/RegisterEvents.kt b/app/src/main/java/com/spoony/spoony/core/analytics/events/RegisterEvents.kt index cf33a8364..30408eb66 100644 --- a/app/src/main/java/com/spoony/spoony/core/analytics/events/RegisterEvents.kt +++ b/app/src/main/java/com/spoony/spoony/core/analytics/events/RegisterEvents.kt @@ -2,6 +2,7 @@ package com.spoony.spoony.core.analytics.events import com.spoony.spoony.core.analytics.MixPanelTracker import jakarta.inject.Inject +import org.json.JSONObject class RegisterEvents @Inject constructor( private val tracker: MixPanelTracker @@ -13,13 +14,11 @@ class RegisterEvents @Inject constructor( ) { tracker.track( eventName = "review_1_completed", - properties = """ - { - "place_name": "$placeName", - "category": "$category", - "menu_count": $menuCount - } - """.trimIndent() + properties = JSONObject().apply { + put("place_name", placeName) + put("category", category) + put("menu_count", menuCount) + } ) } @@ -30,13 +29,11 @@ class RegisterEvents @Inject constructor( ) { tracker.track( eventName = "review_2_completed", - properties = """ - { - "review_length": $reviewLength, - "photo_count": $photoCount, - "has_disappointment": $hasDisappointment - } - """.trimIndent() + properties = JSONObject().apply { + put("review_length", reviewLength) + put("photo_count", photoCount) + put("has_disappointment", hasDisappointment) + } ) } } diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/events/ReviewDetailEvents.kt b/app/src/main/java/com/spoony/spoony/core/analytics/events/ReviewDetailEvents.kt index a650cce1f..61c346862 100644 --- a/app/src/main/java/com/spoony/spoony/core/analytics/events/ReviewDetailEvents.kt +++ b/app/src/main/java/com/spoony/spoony/core/analytics/events/ReviewDetailEvents.kt @@ -2,6 +2,7 @@ package com.spoony.spoony.core.analytics.events import com.spoony.spoony.core.analytics.MixPanelTracker import jakarta.inject.Inject +import org.json.JSONObject class ReviewDetailEvents @Inject constructor( private val tracker: MixPanelTracker @@ -21,21 +22,19 @@ class ReviewDetailEvents @Inject constructor( ) { tracker.track( eventName = "spoon_use_intent", - properties = """ - { - "review_id" : $reviewId, - "author_user_id" : $authorUserId, - "place_name" : "$placeName", - "category" : "$category", - "menu_count" : $menuCount, - "satisfaction_score" : $satisfactionScore, - "review_length" : $reviewLength, - "photo_count" : $photoCount, - "has_disappointment" : $hasDisappointment, - "saved_count" : $savedCount, - "is_following_author" : $isFollowingAuthor - } - """.trimIndent() + properties = JSONObject().apply { + put("review_id", reviewId) + put("author_user_id", authorUserId) + put("place_name", placeName) + put("category", category) + put("menu_count", menuCount) + put("satisfaction_score", satisfactionScore) + put("review_length", reviewLength) + put("photo_count", photoCount) + put("has_disappointment", hasDisappointment) + put("saved_count", savedCount) + put("is_following_author", isFollowingAuthor) + } ) } @@ -54,21 +53,19 @@ class ReviewDetailEvents @Inject constructor( ) { tracker.track( eventName = "spoon_used", - properties = """ - { - "review_id" : $reviewId, - "author_user_id" : $authorUserId, - "place_name" : "$placeName", - "category": "$category", - "menu_count" : $menuCount, - "satisfaction_score" : $satisfactionScore, - "review_length" : $reviewLength, - "photo_count" : $photoCount, - "has_disappointment" : $hasDisappointment, - "saved_count" : $savedCount, - "is_following_author" : $isFollowingAuthor - } - """.trimIndent() + properties = JSONObject().apply { + put("review_id", reviewId) + put("author_user_id", authorUserId) + put("place_name", placeName) + put("category", category) + put("menu_count", menuCount) + put("satisfaction_score", satisfactionScore) + put("review_length", reviewLength) + put("photo_count", photoCount) + put("has_disappointment", hasDisappointment) + put("saved_count", savedCount) + put("is_following_author", isFollowingAuthor) + } ) } @@ -91,21 +88,19 @@ class ReviewDetailEvents @Inject constructor( ) { tracker.track( eventName = "place_map_saved", - properties = """ - { - "review_id" : $reviewId, - "author_user_id" : $authorUserId, - "place_name" : "$placeName", - "category" : "$category", - "menu_count" : $menuCount, - "satisfaction_score" : $satisfactionScore, - "review_length" : $reviewLength, - "photo_count" : $photoCount, - "has_disappointment" : $hasDisappointment, - "saved_count" : $savedCount, - "is_following_author" : $isFollowingAuthor - } - """.trimIndent() + properties = JSONObject().apply { + put("review_id", reviewId) + put("author_user_id", authorUserId) + put("place_name", placeName) + put("category", category) + put("menu_count", menuCount) + put("satisfaction_score", satisfactionScore) + put("review_length", reviewLength) + put("photo_count", photoCount) + put("has_disappointment", hasDisappointment) + put("saved_count", savedCount) + put("is_following_author", isFollowingAuthor) + } ) } @@ -124,21 +119,19 @@ class ReviewDetailEvents @Inject constructor( ) { tracker.track( eventName = "place_map_removed", - properties = """ - { - "review_id" : $reviewId, - "author_user_id" : $authorUserId, - "place_name" : "$placeName", - "category": "$category", - "menu_count" : $menuCount, - "satisfaction_score" : $satisfactionScore, - "review_length" : $reviewLength, - "photo_count" : $photoCount, - "has_disappointment" : $hasDisappointment, - "saved_count" : $savedCount, - "is_following_author" : $isFollowingAuthor - } - """.trimIndent() + properties = JSONObject().apply { + put("review_id", reviewId) + put("author_user_id", authorUserId) + put("place_name", placeName) + put("category", category) + put("menu_count", menuCount) + put("satisfaction_score", satisfactionScore) + put("review_length", reviewLength) + put("photo_count", photoCount) + put("has_disappointment", hasDisappointment) + put("saved_count", savedCount) + put("is_following_author", isFollowingAuthor) + } ) } @@ -157,21 +150,19 @@ class ReviewDetailEvents @Inject constructor( ) { tracker.track( eventName = "direction_clicked", - properties = """ - { - "review_id" : $reviewId, - "author_user_id" : $authorUserId, - "place_name" : "$placeName", - "category": "$category", - "menu_count" : $menuCount, - "satisfaction_score" : $satisfactionScore, - "review_length" : $reviewLength, - "photo_count" : $photoCount, - "has_disappointment" : $hasDisappointment, - "saved_count" : $savedCount, - "is_following_author" : $isFollowingAuthor - } - """.trimIndent() + properties = JSONObject().apply { + put("review_id", reviewId) + put("author_user_id", authorUserId) + put("place_name", placeName) + put("category", category) + put("menu_count", menuCount) + put("satisfaction_score", satisfactionScore) + put("review_length", reviewLength) + put("photo_count", photoCount) + put("has_disappointment", hasDisappointment) + put("saved_count", savedCount) + put("is_following_author", isFollowingAuthor) + } ) } } diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/events/SpoonDrawEvents.kt b/app/src/main/java/com/spoony/spoony/core/analytics/events/SpoonDrawEvents.kt index fec610812..254674239 100644 --- a/app/src/main/java/com/spoony/spoony/core/analytics/events/SpoonDrawEvents.kt +++ b/app/src/main/java/com/spoony/spoony/core/analytics/events/SpoonDrawEvents.kt @@ -2,6 +2,7 @@ package com.spoony.spoony.core.analytics.events import com.spoony.spoony.core.analytics.MixPanelTracker import jakarta.inject.Inject +import org.json.JSONObject class SpoonDrawEvents @Inject constructor( private val tracker: MixPanelTracker @@ -9,11 +10,9 @@ class SpoonDrawEvents @Inject constructor( fun spoonReceived(spoonCount: Int) { tracker.track( eventName = "spoon_received", - properties = """ - { - "spoon_count": $spoonCount - } - """.trimIndent() + properties = JSONObject().apply { + put("spoon_count", spoonCount) + } ) } } From fbe4eca0978b821f25a7c72cc64f2f4515364b41 Mon Sep 17 00:00:00 2001 From: Hyobeen-Park Date: Wed, 8 Oct 2025 06:41:51 +0900 Subject: [PATCH 30/31] =?UTF-8?q?[CHORE/#386]=20mypage=20tabEntered=20?= =?UTF-8?q?=ED=8A=B8=EB=9E=98=ED=82=B9=20=EC=9C=84=EC=B9=98=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 --- .../spoony/presentation/userpage/mypage/MyPageRoute.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/spoony/spoony/presentation/userpage/mypage/MyPageRoute.kt b/app/src/main/java/com/spoony/spoony/presentation/userpage/mypage/MyPageRoute.kt index c33bb6ed5..77c6fc6e3 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/userpage/mypage/MyPageRoute.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/userpage/mypage/MyPageRoute.kt @@ -41,10 +41,6 @@ fun MyPageRoute( LaunchedEffect(Unit) { viewModel.getUserProfile() viewModel.getSpoonCount() - - if (userPageState.userType == UserType.MY_PAGE) { - tracker.commonEvents.tabEntered("mypage") - } } LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { @@ -61,6 +57,12 @@ fun MyPageRoute( } } + LaunchedEffect(userPageState.userType) { + if (userPageState.userType == UserType.MY_PAGE) { + tracker.commonEvents.tabEntered("mypage") + } + } + val userPageEvents = UserPageEvents( onSettingClick = navigateToSettings, onMainButtonClick = navigateToProfileEdit, From ec840e5fade22e1d2efb53d586f06b81c7fe2bd9 Mon Sep 17 00:00:00 2001 From: Hyobeen-Park Date: Thu, 16 Oct 2025 16:53:37 +0900 Subject: [PATCH 31/31] =?UTF-8?q?[MOD/#386]=20ReviewTrackingModel=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spoony/core/analytics/MixPanelTracker.kt | 16 +- .../core/analytics/events/CommonEvents.kt | 127 +++++-------- .../analytics/events/ReviewDetailEvents.kt | 156 ++++++--------- .../analytics/model/ReviewTrackingModel.kt | 14 ++ .../placeDetail/PlaceDetailRoute.kt | 177 ++++++++++-------- .../register/RegisterEndScreen.kt | 23 ++- 6 files changed, 226 insertions(+), 287 deletions(-) create mode 100644 app/src/main/java/com/spoony/spoony/core/analytics/model/ReviewTrackingModel.kt diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt b/app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt index 687589a7b..4d7030805 100644 --- a/app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt +++ b/app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt @@ -4,7 +4,7 @@ import android.content.Context import com.mixpanel.android.mpmetrics.MixpanelAPI import com.spoony.spoony.BuildConfig.MIXPANEL_KEY import dagger.hilt.android.qualifiers.ApplicationContext -import javax.inject.Inject +import jakarta.inject.Inject import org.json.JSONObject import timber.log.Timber @@ -33,22 +33,8 @@ class MixPanelTracker @Inject constructor( mixpanel.track(eventName) } - fun track(eventName: String, properties: String) { - Timber.tag("mixpanel").d("$eventName $properties") - mixpanel.track(eventName, properties.toJsonObject()) - } - fun track(eventName: String, properties: JSONObject) { Timber.tag("mixpanel").d("$eventName $properties") mixpanel.track(eventName, properties) } - - private fun String.toJsonObject(): JSONObject { - return try { - JSONObject(this) - } catch (e: Exception) { - Timber.e(e) - JSONObject() - } - } } diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/events/CommonEvents.kt b/app/src/main/java/com/spoony/spoony/core/analytics/events/CommonEvents.kt index d1cd7fc46..05a7c7a0c 100644 --- a/app/src/main/java/com/spoony/spoony/core/analytics/events/CommonEvents.kt +++ b/app/src/main/java/com/spoony/spoony/core/analytics/events/CommonEvents.kt @@ -1,6 +1,7 @@ package com.spoony.spoony.core.analytics.events import com.spoony.spoony.core.analytics.MixPanelTracker +import com.spoony.spoony.core.analytics.model.ReviewTrackingModel import jakarta.inject.Inject import org.json.JSONArray import org.json.JSONObject @@ -18,34 +19,24 @@ class CommonEvents @Inject constructor( } fun reviewViewed( - reviewId: Int, - authorUserId: Int, - placeName: String, - category: String, - menuCount: Int, - satisfactionScore: Double, - reviewLength: Int, - photoCount: Int, - hasDisappointment: Boolean, - savedCount: Int, + reviewTrackingModel: ReviewTrackingModel, isSelfReview: Boolean, isFollowedUserReview: Boolean, isSavedReview: Boolean -// entryPoint: String ) { tracker.track( eventName = "review_viewed", properties = JSONObject().apply { - put("review_id", reviewId) - put("author_user_id", authorUserId) - put("place_name", placeName) - put("category", category) - put("menu_count", menuCount) - put("satisfaction_score", satisfactionScore) - put("review_length", reviewLength) - put("photo_count", photoCount) - put("has_disappointment", hasDisappointment) - put("saved_count", savedCount) + put("review_id", reviewTrackingModel.reviewId) + put("author_user_id", reviewTrackingModel.authorUserId) + put("place_name", reviewTrackingModel.placeName) + put("category", reviewTrackingModel.category) + put("menu_count", reviewTrackingModel.menuCount) + put("satisfaction_score", reviewTrackingModel.satisfactionScore) + put("review_length", reviewTrackingModel.reviewLength) + put("photo_count", reviewTrackingModel.photoCount) + put("has_disappointment", reviewTrackingModel.hasDisappointment) + put("saved_count", reviewTrackingModel.savedCount) put("is_self_review", isSelfReview) put("is_followed_user_review", isFollowedUserReview) put("is_saved_review", isSavedReview) @@ -54,31 +45,21 @@ class CommonEvents @Inject constructor( } fun reviewEdited( - reviewId: Int, - authorUserId: Int, - placeName: String, - category: String, - menuCount: Int, - satisfactionScore: Float, - reviewLength: Int, - photoCount: Int, - hasDisappointment: Boolean, - savedCount: Int -// entryPoint: String + reviewTrackingModel: ReviewTrackingModel ) { tracker.track( eventName = "review_edited", properties = JSONObject().apply { - put("review_id", reviewId) - put("author_user_id", authorUserId) - put("place_name", placeName) - put("category", category) - put("menu_count", menuCount) - put("satisfaction_score", satisfactionScore) - put("review_length", reviewLength) - put("photo_count", photoCount) - put("has_disappointment", hasDisappointment) - put("saved_count", savedCount) + put("review_id", reviewTrackingModel.reviewId) + put("author_user_id", reviewTrackingModel.authorUserId) + put("place_name", reviewTrackingModel.placeName) + put("category", reviewTrackingModel.category) + put("menu_count", reviewTrackingModel.menuCount) + put("satisfaction_score", reviewTrackingModel.satisfactionScore) + put("review_length", reviewTrackingModel.reviewLength) + put("photo_count", reviewTrackingModel.photoCount) + put("has_disappointment", reviewTrackingModel.hasDisappointment) + put("saved_count", reviewTrackingModel.savedCount) } ) } @@ -126,60 +107,42 @@ class CommonEvents @Inject constructor( } fun followUserFromReview( - reviewId: Int, - authorUserId: Int, - placeName: String, - category: String, - menuCount: Int, - satisfactionScore: Double, - reviewLength: Int, - photoCount: Int, - hasDisappointment: Boolean, - savedCount: Int + reviewTrackingModel: ReviewTrackingModel ) { tracker.track( eventName = "follow_user_from_review", properties = JSONObject().apply { - put("review_id", reviewId) - put("author_user_id", authorUserId) - put("place_name", placeName) - put("category", category) - put("menu_count", menuCount) - put("satisfaction_score", satisfactionScore) - put("review_length", reviewLength) - put("photo_count", photoCount) - put("has_disappointment", hasDisappointment) - put("saved_count", savedCount) + put("review_id", reviewTrackingModel.reviewId) + put("author_user_id", reviewTrackingModel.authorUserId) + put("place_name", reviewTrackingModel.placeName) + put("category", reviewTrackingModel.category) + put("menu_count", reviewTrackingModel.menuCount) + put("satisfaction_score", reviewTrackingModel.satisfactionScore) + put("review_length", reviewTrackingModel.reviewLength) + put("photo_count", reviewTrackingModel.photoCount) + put("has_disappointment", reviewTrackingModel.hasDisappointment) + put("saved_count", reviewTrackingModel.savedCount) put("entry_point", "review") } ) } fun unfollowUserFromReview( - reviewId: Int, - authorUserId: Int, - placeName: String, - category: String, - menuCount: Int, - satisfactionScore: Double, - reviewLength: Int, - photoCount: Int, - hasDisappointment: Boolean, - savedCount: Int + reviewTrackingModel: ReviewTrackingModel ) { tracker.track( eventName = "unfollow_user_from_review", properties = JSONObject().apply { - put("review_id", reviewId) - put("author_user_id", authorUserId) - put("place_name", placeName) - put("category", category) - put("menu_count", menuCount) - put("satisfaction_score", satisfactionScore) - put("review_length", reviewLength) - put("photo_count", photoCount) - put("has_disappointment", hasDisappointment) - put("saved_count", savedCount) + put("review_id", reviewTrackingModel.reviewId) + put("author_user_id", reviewTrackingModel.authorUserId) + put("place_name", reviewTrackingModel.placeName) + put("category", reviewTrackingModel.category) + put("menu_count", reviewTrackingModel.menuCount) + put("satisfaction_score", reviewTrackingModel.satisfactionScore) + put("review_length", reviewTrackingModel.reviewLength) + put("photo_count", reviewTrackingModel.photoCount) + put("has_disappointment", reviewTrackingModel.hasDisappointment) + put("saved_count", reviewTrackingModel.savedCount) put("entry_point", "review") } ) diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/events/ReviewDetailEvents.kt b/app/src/main/java/com/spoony/spoony/core/analytics/events/ReviewDetailEvents.kt index 61c346862..ba3f557ac 100644 --- a/app/src/main/java/com/spoony/spoony/core/analytics/events/ReviewDetailEvents.kt +++ b/app/src/main/java/com/spoony/spoony/core/analytics/events/ReviewDetailEvents.kt @@ -1,6 +1,7 @@ package com.spoony.spoony.core.analytics.events import com.spoony.spoony.core.analytics.MixPanelTracker +import com.spoony.spoony.core.analytics.model.ReviewTrackingModel import jakarta.inject.Inject import org.json.JSONObject @@ -8,62 +9,44 @@ class ReviewDetailEvents @Inject constructor( private val tracker: MixPanelTracker ) { fun spoonUseIntent( - reviewId: Int, - authorUserId: Int, - placeName: String, - category: String, - menuCount: Int, - satisfactionScore: Double, - reviewLength: Int, - photoCount: Int, - hasDisappointment: Boolean, - savedCount: Int, + reviewTrackingModel: ReviewTrackingModel, isFollowingAuthor: Boolean ) { tracker.track( eventName = "spoon_use_intent", properties = JSONObject().apply { - put("review_id", reviewId) - put("author_user_id", authorUserId) - put("place_name", placeName) - put("category", category) - put("menu_count", menuCount) - put("satisfaction_score", satisfactionScore) - put("review_length", reviewLength) - put("photo_count", photoCount) - put("has_disappointment", hasDisappointment) - put("saved_count", savedCount) + put("review_id", reviewTrackingModel.reviewId) + put("author_user_id", reviewTrackingModel.authorUserId) + put("place_name", reviewTrackingModel.placeName) + put("category", reviewTrackingModel.category) + put("menu_count", reviewTrackingModel.menuCount) + put("satisfaction_score", reviewTrackingModel.satisfactionScore) + put("review_length", reviewTrackingModel.reviewLength) + put("photo_count", reviewTrackingModel.photoCount) + put("has_disappointment", reviewTrackingModel.hasDisappointment) + put("saved_count", reviewTrackingModel.savedCount) put("is_following_author", isFollowingAuthor) } ) } fun spoonUsed( - reviewId: Int, - authorUserId: Int, - placeName: String, - category: String, - menuCount: Int, - satisfactionScore: Double, - reviewLength: Int, - photoCount: Int, - hasDisappointment: Boolean, - savedCount: Int, + reviewTrackingModel: ReviewTrackingModel, isFollowingAuthor: Boolean ) { tracker.track( eventName = "spoon_used", properties = JSONObject().apply { - put("review_id", reviewId) - put("author_user_id", authorUserId) - put("place_name", placeName) - put("category", category) - put("menu_count", menuCount) - put("satisfaction_score", satisfactionScore) - put("review_length", reviewLength) - put("photo_count", photoCount) - put("has_disappointment", hasDisappointment) - put("saved_count", savedCount) + put("review_id", reviewTrackingModel.reviewId) + put("author_user_id", reviewTrackingModel.authorUserId) + put("place_name", reviewTrackingModel.placeName) + put("category", reviewTrackingModel.category) + put("menu_count", reviewTrackingModel.menuCount) + put("satisfaction_score", reviewTrackingModel.satisfactionScore) + put("review_length", reviewTrackingModel.reviewLength) + put("photo_count", reviewTrackingModel.photoCount) + put("has_disappointment", reviewTrackingModel.hasDisappointment) + put("saved_count", reviewTrackingModel.savedCount) put("is_following_author", isFollowingAuthor) } ) @@ -74,93 +57,66 @@ class ReviewDetailEvents @Inject constructor( } fun placeMapSaved( - reviewId: Int, - authorUserId: Int, - placeName: String, - category: String, - menuCount: Int, - satisfactionScore: Double, - reviewLength: Int, - photoCount: Int, - hasDisappointment: Boolean, - savedCount: Int, + reviewTrackingModel: ReviewTrackingModel, isFollowingAuthor: Boolean ) { tracker.track( eventName = "place_map_saved", properties = JSONObject().apply { - put("review_id", reviewId) - put("author_user_id", authorUserId) - put("place_name", placeName) - put("category", category) - put("menu_count", menuCount) - put("satisfaction_score", satisfactionScore) - put("review_length", reviewLength) - put("photo_count", photoCount) - put("has_disappointment", hasDisappointment) - put("saved_count", savedCount) + put("review_id", reviewTrackingModel.reviewId) + put("author_user_id", reviewTrackingModel.authorUserId) + put("place_name", reviewTrackingModel.placeName) + put("category", reviewTrackingModel.category) + put("menu_count", reviewTrackingModel.menuCount) + put("satisfaction_score", reviewTrackingModel.satisfactionScore) + put("review_length", reviewTrackingModel.reviewLength) + put("photo_count", reviewTrackingModel.photoCount) + put("has_disappointment", reviewTrackingModel.hasDisappointment) + put("saved_count", reviewTrackingModel.savedCount) put("is_following_author", isFollowingAuthor) } ) } fun placeMapRemoved( - reviewId: Int, - authorUserId: Int, - placeName: String, - category: String, - menuCount: Int, - satisfactionScore: Double, - reviewLength: Int, - photoCount: Int, - hasDisappointment: Boolean, - savedCount: Int, + reviewTrackingModel: ReviewTrackingModel, isFollowingAuthor: Boolean ) { tracker.track( eventName = "place_map_removed", properties = JSONObject().apply { - put("review_id", reviewId) - put("author_user_id", authorUserId) - put("place_name", placeName) - put("category", category) - put("menu_count", menuCount) - put("satisfaction_score", satisfactionScore) - put("review_length", reviewLength) - put("photo_count", photoCount) - put("has_disappointment", hasDisappointment) - put("saved_count", savedCount) + put("review_id", reviewTrackingModel.reviewId) + put("author_user_id", reviewTrackingModel.authorUserId) + put("place_name", reviewTrackingModel.placeName) + put("category", reviewTrackingModel.category) + put("menu_count", reviewTrackingModel.menuCount) + put("satisfaction_score", reviewTrackingModel.satisfactionScore) + put("review_length", reviewTrackingModel.reviewLength) + put("photo_count", reviewTrackingModel.photoCount) + put("has_disappointment", reviewTrackingModel.hasDisappointment) + put("saved_count", reviewTrackingModel.savedCount) put("is_following_author", isFollowingAuthor) } ) } fun directionClicked( - reviewId: Int, - authorUserId: Int, - placeName: String, - category: String, - menuCount: Int, - satisfactionScore: Double, - reviewLength: Int, - photoCount: Int, - hasDisappointment: Boolean, - savedCount: Int, + reviewTrackingModel: ReviewTrackingModel, isFollowingAuthor: Boolean ) { tracker.track( eventName = "direction_clicked", properties = JSONObject().apply { - put("review_id", reviewId) - put("author_user_id", authorUserId) - put("place_name", placeName) - put("category", category) - put("menu_count", menuCount) - put("satisfaction_score", satisfactionScore) - put("review_length", reviewLength) - put("photo_count", photoCount) - put("has_disappointment", hasDisappointment) - put("saved_count", savedCount) + put("review_id", reviewTrackingModel.reviewId) + put("author_user_id", reviewTrackingModel.authorUserId) + put("place_name", reviewTrackingModel.placeName) + put("category", reviewTrackingModel.category) + put("menu_count", reviewTrackingModel.menuCount) + put("satisfaction_score", reviewTrackingModel.satisfactionScore) + put("review_length", reviewTrackingModel.reviewLength) + put("photo_count", reviewTrackingModel.photoCount) + put("has_disappointment", reviewTrackingModel.hasDisappointment) + put("saved_count", reviewTrackingModel.savedCount) put("is_following_author", isFollowingAuthor) } ) diff --git a/app/src/main/java/com/spoony/spoony/core/analytics/model/ReviewTrackingModel.kt b/app/src/main/java/com/spoony/spoony/core/analytics/model/ReviewTrackingModel.kt new file mode 100644 index 000000000..dccf878f3 --- /dev/null +++ b/app/src/main/java/com/spoony/spoony/core/analytics/model/ReviewTrackingModel.kt @@ -0,0 +1,14 @@ +package com.spoony.spoony.core.analytics.model + +data class ReviewTrackingModel( + val reviewId: Int, + val authorUserId: Int, + val placeName: String, + val category: String, + val menuCount: Int, + val satisfactionScore: Double, + val reviewLength: Int, + val photoCount: Int, + val hasDisappointment: Boolean, + val savedCount: Int +) diff --git a/app/src/main/java/com/spoony/spoony/presentation/placeDetail/PlaceDetailRoute.kt b/app/src/main/java/com/spoony/spoony/presentation/placeDetail/PlaceDetailRoute.kt index 8bec7e0d3..61e7af321 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/placeDetail/PlaceDetailRoute.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/placeDetail/PlaceDetailRoute.kt @@ -39,6 +39,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.repeatOnLifecycle import com.spoony.spoony.core.analytics.events.LocalTracker +import com.spoony.spoony.core.analytics.model.ReviewTrackingModel import com.spoony.spoony.core.designsystem.component.button.FollowButton import com.spoony.spoony.core.designsystem.component.snackbar.TextSnackbar import com.spoony.spoony.core.designsystem.component.topappbar.TagTopAppBar @@ -140,16 +141,18 @@ fun PlaceDetailRoute( val uiState = state.placeDetailModel if (uiState is UiState.Success) { tracker.commonEvents.reviewViewed( - reviewId = (state.reviewId as UiState.Success).data, - authorUserId = userProfile.userId, - placeName = uiState.data.placeName, - category = uiState.data.category.categoryName, - menuCount = uiState.data.menuList.size, - satisfactionScore = uiState.data.value, - reviewLength = uiState.data.description.length, - photoCount = uiState.data.photoUrlList.size, - hasDisappointment = uiState.data.cons.isNotEmpty(), - savedCount = state.addMapCount, + reviewTrackingModel = ReviewTrackingModel( + reviewId = (state.reviewId as UiState.Success).data, + authorUserId = userProfile.userId, + placeName = uiState.data.placeName, + category = uiState.data.category.categoryName, + menuCount = uiState.data.menuList.size, + satisfactionScore = uiState.data.value, + reviewLength = uiState.data.description.length, + photoCount = uiState.data.photoUrlList.size, + hasDisappointment = uiState.data.cons.isNotEmpty(), + savedCount = state.addMapCount + ), isSelfReview = uiState.data.isMine, isFollowedUserReview = state.isFollowing, isSavedReview = state.isAddMap @@ -170,16 +173,18 @@ fun PlaceDetailRoute( viewModel.useSpoon(postId) scoopDialogVisibility = false tracker.reviewDetailEvents.spoonUsed( - reviewId = postId, - authorUserId = userProfile.userId, - placeName = uiState.data.placeName, - category = uiState.data.category.categoryName, - menuCount = uiState.data.menuList.size, - satisfactionScore = uiState.data.value, - reviewLength = uiState.data.description.length, - photoCount = uiState.data.photoUrlList.size, - hasDisappointment = uiState.data.cons.isNotEmpty(), - savedCount = state.addMapCount, + reviewTrackingModel = ReviewTrackingModel( + reviewId = (state.reviewId as UiState.Success).data, + authorUserId = userProfile.userId, + placeName = uiState.data.placeName, + category = uiState.data.category.categoryName, + menuCount = uiState.data.menuList.size, + satisfactionScore = uiState.data.value, + reviewLength = uiState.data.description.length, + photoCount = uiState.data.photoUrlList.size, + hasDisappointment = uiState.data.cons.isNotEmpty(), + savedCount = state.addMapCount + ), isFollowingAuthor = state.isFollowing ) }, @@ -230,16 +235,18 @@ fun PlaceDetailRoute( isAddMap = state.isAddMap, onSearchMapClick = { tracker.reviewDetailEvents.directionClicked( - reviewId = postId, - authorUserId = userProfile.userId, - placeName = uiState.data.placeName, - category = uiState.data.category.categoryName, - menuCount = uiState.data.menuList.size, - satisfactionScore = uiState.data.value, - reviewLength = uiState.data.description.length, - photoCount = uiState.data.photoUrlList.size, - hasDisappointment = uiState.data.cons.isNotEmpty(), - savedCount = state.addMapCount, + reviewTrackingModel = ReviewTrackingModel( + reviewId = (state.reviewId as UiState.Success).data, + authorUserId = userProfile.userId, + placeName = uiState.data.placeName, + category = uiState.data.category.categoryName, + menuCount = uiState.data.menuList.size, + satisfactionScore = uiState.data.value, + reviewLength = uiState.data.description.length, + photoCount = uiState.data.photoUrlList.size, + hasDisappointment = uiState.data.cons.isNotEmpty(), + savedCount = state.addMapCount + ), isFollowingAuthor = state.isFollowing ) searchPlaceNaverMap( @@ -253,32 +260,36 @@ fun PlaceDetailRoute( onAddMapButtonClick = { viewModel.addMyMap(postId) tracker.reviewDetailEvents.placeMapSaved( - reviewId = postId, - authorUserId = userProfile.userId, - placeName = uiState.data.placeName, - category = uiState.data.category.categoryName, - menuCount = uiState.data.menuList.size, - satisfactionScore = uiState.data.value, - reviewLength = uiState.data.description.length, - photoCount = uiState.data.photoUrlList.size, - hasDisappointment = uiState.data.cons.isNotEmpty(), - savedCount = state.addMapCount, + reviewTrackingModel = ReviewTrackingModel( + reviewId = (state.reviewId as UiState.Success).data, + authorUserId = userProfile.userId, + placeName = uiState.data.placeName, + category = uiState.data.category.categoryName, + menuCount = uiState.data.menuList.size, + satisfactionScore = uiState.data.value, + reviewLength = uiState.data.description.length, + photoCount = uiState.data.photoUrlList.size, + hasDisappointment = uiState.data.cons.isNotEmpty(), + savedCount = state.addMapCount + ), isFollowingAuthor = state.isFollowing ) }, onDeletePinMapButtonClick = { viewModel.deletePinMap(postId) tracker.reviewDetailEvents.placeMapRemoved( - reviewId = postId, - authorUserId = userProfile.userId, - placeName = uiState.data.placeName, - category = uiState.data.category.categoryName, - menuCount = uiState.data.menuList.size, - satisfactionScore = uiState.data.value, - reviewLength = uiState.data.description.length, - photoCount = uiState.data.photoUrlList.size, - hasDisappointment = uiState.data.cons.isNotEmpty(), - savedCount = state.addMapCount, + reviewTrackingModel = ReviewTrackingModel( + reviewId = (state.reviewId as UiState.Success).data, + authorUserId = userProfile.userId, + placeName = uiState.data.placeName, + category = uiState.data.category.categoryName, + menuCount = uiState.data.menuList.size, + satisfactionScore = uiState.data.value, + reviewLength = uiState.data.description.length, + photoCount = uiState.data.photoUrlList.size, + hasDisappointment = uiState.data.cons.isNotEmpty(), + savedCount = state.addMapCount + ), isFollowingAuthor = state.isFollowing ) } @@ -314,29 +325,33 @@ fun PlaceDetailRoute( if (state.isFollowing) { tracker.commonEvents.unfollowUserFromReview( - reviewId = postId, - authorUserId = userProfile.userId, - placeName = uiState.data.placeName, - category = uiState.data.category.categoryName, - menuCount = uiState.data.menuList.size, - satisfactionScore = uiState.data.value, - reviewLength = uiState.data.description.length, - photoCount = uiState.data.photoUrlList.size, - hasDisappointment = uiState.data.cons.isNotEmpty(), - savedCount = state.addMapCount + reviewTrackingModel = ReviewTrackingModel( + reviewId = (state.reviewId as UiState.Success).data, + authorUserId = userProfile.userId, + placeName = uiState.data.placeName, + category = uiState.data.category.categoryName, + menuCount = uiState.data.menuList.size, + satisfactionScore = uiState.data.value, + reviewLength = uiState.data.description.length, + photoCount = uiState.data.photoUrlList.size, + hasDisappointment = uiState.data.cons.isNotEmpty(), + savedCount = state.addMapCount + ) ) } else { tracker.commonEvents.followUserFromReview( - reviewId = postId, - authorUserId = userProfile.userId, - placeName = uiState.data.placeName, - category = uiState.data.category.categoryName, - menuCount = uiState.data.menuList.size, - satisfactionScore = uiState.data.value, - reviewLength = uiState.data.description.length, - photoCount = uiState.data.photoUrlList.size, - hasDisappointment = uiState.data.cons.isNotEmpty(), - savedCount = state.addMapCount + reviewTrackingModel = ReviewTrackingModel( + reviewId = (state.reviewId as UiState.Success).data, + authorUserId = userProfile.userId, + placeName = uiState.data.placeName, + category = uiState.data.category.categoryName, + menuCount = uiState.data.menuList.size, + satisfactionScore = uiState.data.value, + reviewLength = uiState.data.description.length, + photoCount = uiState.data.photoUrlList.size, + hasDisappointment = uiState.data.cons.isNotEmpty(), + savedCount = state.addMapCount + ) ) } }, @@ -352,16 +367,18 @@ fun PlaceDetailRoute( onShowSnackBar = viewModel::showSnackBar, trackSpoonUseIntent = { tracker.reviewDetailEvents.spoonUseIntent( - reviewId = postId, - authorUserId = userProfile.userId, - placeName = uiState.data.placeName, - category = uiState.data.category.categoryName, - menuCount = uiState.data.menuList.size, - satisfactionScore = uiState.data.value, - reviewLength = uiState.data.description.length, - photoCount = uiState.data.photoUrlList.size, - hasDisappointment = uiState.data.cons.isNotEmpty(), - savedCount = state.addMapCount, + reviewTrackingModel = ReviewTrackingModel( + reviewId = (state.reviewId as UiState.Success).data, + authorUserId = userProfile.userId, + placeName = uiState.data.placeName, + category = uiState.data.category.categoryName, + menuCount = uiState.data.menuList.size, + satisfactionScore = uiState.data.value, + reviewLength = uiState.data.description.length, + photoCount = uiState.data.photoUrlList.size, + hasDisappointment = uiState.data.cons.isNotEmpty(), + savedCount = state.addMapCount + ), isFollowingAuthor = state.isFollowing ) }, diff --git a/app/src/main/java/com/spoony/spoony/presentation/register/RegisterEndScreen.kt b/app/src/main/java/com/spoony/spoony/presentation/register/RegisterEndScreen.kt index 99eacb52a..38f687238 100644 --- a/app/src/main/java/com/spoony/spoony/presentation/register/RegisterEndScreen.kt +++ b/app/src/main/java/com/spoony/spoony/presentation/register/RegisterEndScreen.kt @@ -25,6 +25,7 @@ import com.airbnb.lottie.compose.LottieConstants import com.airbnb.lottie.compose.rememberLottieComposition import com.spoony.spoony.R import com.spoony.spoony.core.analytics.events.LocalTracker +import com.spoony.spoony.core.analytics.model.ReviewTrackingModel import com.spoony.spoony.core.designsystem.component.dialog.SingleButtonDialog import com.spoony.spoony.core.designsystem.component.textfield.SpoonyLargeTextField import com.spoony.spoony.core.designsystem.theme.SpoonyAndroidTheme @@ -82,16 +83,18 @@ fun RegisterEndRoute( onEditComplete = { postId -> onEditComplete(postId) tracker.commonEvents.reviewEdited( - reviewId = postId, - authorUserId = state.userId, - placeName = state.selectedPlace.placeName, - category = state.selectedCategory.categoryName, - menuCount = state.menuList.size, - satisfactionScore = state.userSatisfactionValue, - reviewLength = state.detailReview.length, - photoCount = state.selectedPhotos.size, - hasDisappointment = state.optionalReview.isNotEmpty(), - savedCount = state.addMapCount + reviewTrackingModel = ReviewTrackingModel( + reviewId = postId, + authorUserId = state.userId, + placeName = state.selectedPlace.placeName, + category = state.selectedCategory.categoryName, + menuCount = state.menuList.size, + satisfactionScore = state.userSatisfactionValue.toDouble(), + reviewLength = state.detailReview.length, + photoCount = state.selectedPhotos.size, + hasDisappointment = state.optionalReview.isNotEmpty(), + savedCount = state.addMapCount + ) ) }, postId = viewModel.postId,