Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions app/src/main/java/com/sseotdabwa/buyornot/ui/BuyOrNotViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,23 @@ package com.sseotdabwa.buyornot.ui

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.sseotdabwa.buyornot.core.analytics.Analytics
import com.sseotdabwa.buyornot.domain.repository.AppPreferencesRepository
import com.sseotdabwa.buyornot.domain.repository.UserPreferencesRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class BuyOrNotViewModel @Inject constructor(
private val appPreferencesRepository: AppPreferencesRepository,
private val userPreferencesRepository: UserPreferencesRepository,
private val analytics: Analytics,
) : ViewModel() {
val isFirstRun =
appPreferencesRepository.isFirstRun
Expand All @@ -21,6 +28,14 @@ class BuyOrNotViewModel @Inject constructor(
initialValue = false,
)

init {
userPreferencesRepository.userId
.distinctUntilChanged()
.onEach { userId ->
analytics.identify(if (userId != 0L) userId.toString() else null)
}.launchIn(viewModelScope)
}

fun updateIsFirstRun(isFirstRun: Boolean) {
viewModelScope.launch {
appPreferencesRepository.updateIsFirstRun(isFirstRun)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ package com.sseotdabwa.buyornot.core.analytics

interface Analytics {
fun track(event: AnalyticsEvent)

fun identify(userId: String?)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,8 @@ class DebugAnalytics : Analytics {
override fun track(event: AnalyticsEvent) {
Log.d("Analytics", event.toString())
}

override fun identify(userId: String?) {
Log.d("Analytics", "identify: userId=$userId")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,32 @@ import org.json.JSONObject

class MixpanelAnalytics(
private val mixpanel: MixpanelAPI,
appVersion: String,
) : Analytics {
init {
mixpanel.registerSuperProperties(
JSONObject().apply {
put("platform", "android")
put("app_version", appVersion)
put("user_id", JSONObject.NULL)
},
)
}

override fun track(event: AnalyticsEvent) {
val (name, props) = event.toMixpanel()
mixpanel.track(name, props)
}

override fun identify(userId: String?) {
if (userId != null) {
mixpanel.identify(userId)
}
mixpanel.registerSuperProperties(
JSONObject().apply { put("user_id", userId ?: JSONObject.NULL) },
)
}
Comment thread
DongChyeon marked this conversation as resolved.

private fun AnalyticsEvent.toMixpanel(): Pair<String, JSONObject> {
val props = JSONObject()
val name =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ object AnalyticsModule {
BuildConfig.MIXPANEL_TOKEN,
true,
)
MixpanelAnalytics(mixpanel)
val appVersion =
context.packageManager
.getPackageInfo(context.packageName, 0)
.versionName
?: "unknown"
MixpanelAnalytics(mixpanel, appVersion)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,16 @@ class UserPreferencesRepositoryImpl @Inject constructor(
override val userType: Flow<UserType> =
userPreferencesDataSource.userType.map { it.toDomain() }

override val userId: Flow<Long> = userPreferencesDataSource.userId

override suspend fun updateUserType(userType: UserType) {
userPreferencesDataSource.updateUserType(userType.toDatastore())
}

override suspend fun updateUserId(userId: Long) {
userPreferencesDataSource.updateUserId(userId)
}

override suspend fun updateDisplayName(newName: String) {
userPreferencesDataSource.updateDisplayName(newName)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ enum class UserType {
* 사용자 프로필 및 인증 토큰을 관리합니다.
*/
data class UserPreferences(
val userId: Long = 0L,
val displayName: String = "손님",
val profileImageUrl: String = "",
val accessToken: String = "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ interface UserPreferencesDataSource {

val userType: Flow<UserType>

val userId: Flow<Long>

suspend fun updateUserId(userId: Long)

suspend fun updateDisplayName(newName: String)

suspend fun updateProfileImageUrl(newUrl: String)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.sseotdabwa.buyornot.core.datastore

import android.content.Context
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import dagger.hilt.android.qualifiers.ApplicationContext
Expand All @@ -22,6 +23,7 @@ class UserPreferencesDataSourceImpl @Inject constructor(
@ApplicationContext private val context: Context,
) : UserPreferencesDataSource {
private object Keys {
val USER_ID = longPreferencesKey("user_id")
val DISPLAY_NAME = stringPreferencesKey("display_name")
val PROFILE_IMAGE_URL = stringPreferencesKey("profile_image_url")
val ACCESS_TOKEN = stringPreferencesKey("access_token")
Expand All @@ -32,6 +34,7 @@ class UserPreferencesDataSourceImpl @Inject constructor(
override val preferences: Flow<UserPreferences> =
context.userPreferencesDataStore.data.map { prefs ->
UserPreferences(
userId = prefs[Keys.USER_ID] ?: UserPreferences().userId,
displayName = prefs[Keys.DISPLAY_NAME] ?: UserPreferences().displayName,
profileImageUrl = prefs[Keys.PROFILE_IMAGE_URL] ?: UserPreferences().profileImageUrl,
accessToken = prefs[Keys.ACCESS_TOKEN] ?: UserPreferences().accessToken,
Expand All @@ -47,6 +50,9 @@ class UserPreferencesDataSourceImpl @Inject constructor(
)
}

override val userId: Flow<Long> =
context.userPreferencesDataStore.data.map { it[Keys.USER_ID] ?: 0L }

override val accessToken: Flow<String> = context.userPreferencesDataStore.data.map { it[Keys.ACCESS_TOKEN] ?: "" }

override val userType: Flow<UserType> =
Expand All @@ -60,6 +66,12 @@ class UserPreferencesDataSourceImpl @Inject constructor(
} ?: UserType.GUEST
}

override suspend fun updateUserId(userId: Long) {
context.userPreferencesDataStore.edit { prefs ->
prefs[Keys.USER_ID] = userId
}
}

override suspend fun updateDisplayName(newName: String) {
context.userPreferencesDataStore.edit { prefs ->
prefs[Keys.DISPLAY_NAME] = newName
Expand Down Expand Up @@ -90,6 +102,7 @@ class UserPreferencesDataSourceImpl @Inject constructor(

override suspend fun clearUserInfo() {
context.userPreferencesDataStore.edit { prefs ->
prefs.remove(Keys.USER_ID)
prefs.remove(Keys.ACCESS_TOKEN)
prefs.remove(Keys.REFRESH_TOKEN)
prefs.remove(Keys.PROFILE_IMAGE_URL)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,21 @@ interface UserPreferencesRepository {
*/
val userType: Flow<UserType>

/**
* 현재 로그인된 사용자 ID를 Flow로 제공 (비로그인 시 0L)
*/
val userId: Flow<Long>

/**
* 사용자 타입 업데이트
*/
suspend fun updateUserType(userType: UserType)

/**
* 사용자 ID 업데이트
*/
suspend fun updateUserId(userId: Long)

/**
* 표시 이름 업데이트
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ class LoginViewModel @Inject constructor(
runCatchingCancellable {
userRepository.getMyProfile()
}.onSuccess { profile ->
userPreferencesRepository.updateUserId(profile.id)
userPreferencesRepository.updateDisplayName(profile.nickname)
userPreferencesRepository.updateProfileImageUrl(profile.profileImage)
}.onFailure { e ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
Expand All @@ -60,6 +60,8 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LifecycleEventEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.sseotdabwa.buyornot.core.designsystem.components.ButtonSize
import com.sseotdabwa.buyornot.core.designsystem.components.BuyOrNotAlertDialog
Expand Down Expand Up @@ -365,17 +367,22 @@ private fun HomeFeedList(
val isEmptyViewVisible = filteredFeeds.isEmpty() && !uiState.isLoading && !uiState.hasError
val isMyFeedEmpty = uiState.selectedTab == HomeTab.MY_FEED && isEmptyViewVisible

val enterTimeMs = remember { System.currentTimeMillis() }
DisposableEffect(Unit) {
var enterTimeMs by remember { mutableLongStateOf(System.currentTimeMillis()) }
LifecycleEventEffect(Lifecycle.Event.ON_START) {
enterTimeMs = System.currentTimeMillis()
onIntent(HomeIntent.OnFeedScreenEntered(firstVisibleItemIndex = listState.firstVisibleItemIndex))
onDispose {
onIntent(
HomeIntent.OnFeedScreenExited(
lastVisibleItemIndex = listState.firstVisibleItemIndex,
timeSpentSeconds = (System.currentTimeMillis() - enterTimeMs) / 1000f,
),
)
}
}
LifecycleEventEffect(Lifecycle.Event.ON_STOP) {
onIntent(
HomeIntent.OnFeedScreenExited(
lastVisibleItemIndex =
listState.layoutInfo.visibleItemsInfo
.lastOrNull()
?.index
?: listState.firstVisibleItemIndex,
timeSpentSeconds = (System.currentTimeMillis() - enterTimeMs) / 1000f,
),
)
}
var headerHeightPx by remember { mutableIntStateOf(0) }
val density = LocalDensity.current
Expand Down
Loading