From b40f2de3ad68a09a1f0884f8596d56f2e55097d9 Mon Sep 17 00:00:00 2001 From: oilbeaneda <20221444@sungshin.ac.kr> Date: Wed, 22 Jul 2026 11:15:12 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[setting/#58]=20Firebase=20Analytics=20SDK?= =?UTF-8?q?=20=EB=B0=B0=EC=84=A0=20+=20versionCode=203?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +++ app/build.gradle.kts | 6 +++++- gradle/libs.versions.toml | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index d4bb311..b38ee2b 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,6 @@ local.properties # 서명 키(keystore) — 보안상 절대 커밋 금지 *.jks *.keystore + +# Firebase (google-services.json에 API 키 포함 — 로컬/CI에 각자 배치) +google-services.json diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8da2a70..8209438 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -8,7 +8,7 @@ plugins { alias(libs.plugins.hilt) alias(libs.plugins.ksp) alias(libs.plugins.kotlin.parcelize) - // TODO alias(libs.plugins.google.services) + alias(libs.plugins.google.services) } val properties = Properties().apply { @@ -166,4 +166,8 @@ dependencies { // Naver Login implementation(libs.naver.login) + + // Firebase (Analytics) + implementation(platform(libs.firebase.bom)) + implementation(libs.firebase.analytics) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a11ca9e..d23a5e3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ compileSdk = "37" minSdk = "28" targetSdk = "36" -versionCode = "2" +versionCode = "3" versionName = "1.0.0" From 6e3ca929be25afa5678192c402e1eeec253f0692 Mon Sep 17 00:00:00 2001 From: oilbeaneda <20221444@sungshin.ac.kr> Date: Wed, 22 Jul 2026 11:15:13 +0900 Subject: [PATCH 2/3] =?UTF-8?q?[setting/#58]=20Analytics=20=EC=B6=94?= =?UTF-8?q?=EC=83=81=ED=99=94(AnalyticsHelper/Firebase=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84/Hilt=20=EB=AA=A8=EB=93=88)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/app/core/analytics/AnalyticsHelper.kt | 46 +++++++++++++++++++ .../core/analytics/FirebaseAnalyticsHelper.kt | 36 +++++++++++++++ .../app/core/analytics/di/AnalyticsModule.kt | 29 ++++++++++++ 3 files changed, 111 insertions(+) create mode 100644 app/src/main/java/org/app/core/analytics/AnalyticsHelper.kt create mode 100644 app/src/main/java/org/app/core/analytics/FirebaseAnalyticsHelper.kt create mode 100644 app/src/main/java/org/app/core/analytics/di/AnalyticsModule.kt diff --git a/app/src/main/java/org/app/core/analytics/AnalyticsHelper.kt b/app/src/main/java/org/app/core/analytics/AnalyticsHelper.kt new file mode 100644 index 0000000..0f0f657 --- /dev/null +++ b/app/src/main/java/org/app/core/analytics/AnalyticsHelper.kt @@ -0,0 +1,46 @@ +package org.app.core.analytics + +/** + * 앱 전역 이벤트 로깅 추상화. 구현체는 Firebase Analytics([FirebaseAnalyticsHelper]). + * ViewModel 등에서 이 인터페이스만 주입받아 사용한다 (Firebase 직접 의존 X → 테스트/교체 용이). + */ +interface AnalyticsHelper { + fun logEvent( + name: String, + params: Map = emptyMap(), + ) +} + +/** 커스텀 이벤트 이름 (지표 체계 기준). Firebase 규칙: 영문/숫자/_ , 40자 이내. */ +object AnalyticsEvent { + /** 핵심지표: 펍 상세를 1회+ 조회 */ + const val PUB_DETAIL_VIEW = "pub_detail_view" + + /** Activation: 온보딩(닉네임+응원팀) 완료 */ + const val ONBOARDING_COMPLETE = "onboarding_complete" + + /** Activation: 필터 적용 */ + const val FILTER_APPLY = "filter_apply" + + /** Activation: 검색 실행 */ + const val MAP_SEARCH = "map_search" + + /** Referral/참여: 제보 전송 */ + const val REPORT_SUBMIT = "report_submit" + + /** Referral/참여: 찜(즐겨찾기) 추가 */ + const val FAVORITE_ADD = "favorite_add" + + /** Engagement: 지도 링크(카카오/네이버) 클릭 — 전환에 가장 근접 */ + const val MAP_LINK_CLICK = "map_link_click" +} + +/** 자주 쓰는 파라미터 키 */ +object AnalyticsParam { + const val PUB_ID = "pub_id" + const val MAP_TYPE = "map_type" + const val CATEGORY = "category" + const val TEAM_COUNT = "team_count" + const val REGION_COUNT = "region_count" + const val RESULT_COUNT = "result_count" +} diff --git a/app/src/main/java/org/app/core/analytics/FirebaseAnalyticsHelper.kt b/app/src/main/java/org/app/core/analytics/FirebaseAnalyticsHelper.kt new file mode 100644 index 0000000..435db5f --- /dev/null +++ b/app/src/main/java/org/app/core/analytics/FirebaseAnalyticsHelper.kt @@ -0,0 +1,36 @@ +package org.app.core.analytics + +import com.google.firebase.analytics.FirebaseAnalytics +import com.google.firebase.analytics.logEvent +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Firebase Analytics 기반 [AnalyticsHelper] 구현. + * 설치/DAU/리텐션/세션 등 표준 지표는 SDK가 자동 수집하고, 여기선 커스텀 이벤트만 전송한다. + */ +@Singleton +class FirebaseAnalyticsHelper + @Inject + constructor( + private val firebaseAnalytics: FirebaseAnalytics, + ) : AnalyticsHelper { + override fun logEvent( + name: String, + params: Map, + ) { + firebaseAnalytics.logEvent(name) { + params.forEach { (key, value) -> + when (value) { + is String -> param(key, value) + is Long -> param(key, value) + is Int -> param(key, value.toLong()) + is Double -> param(key, value) + is Float -> param(key, value.toDouble()) + is Boolean -> param(key, value.toString()) + else -> param(key, value.toString()) + } + } + } + } + } diff --git a/app/src/main/java/org/app/core/analytics/di/AnalyticsModule.kt b/app/src/main/java/org/app/core/analytics/di/AnalyticsModule.kt new file mode 100644 index 0000000..d9f7539 --- /dev/null +++ b/app/src/main/java/org/app/core/analytics/di/AnalyticsModule.kt @@ -0,0 +1,29 @@ +package org.app.core.analytics.di + +import android.content.Context +import com.google.firebase.analytics.FirebaseAnalytics +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import org.app.core.analytics.AnalyticsHelper +import org.app.core.analytics.FirebaseAnalyticsHelper +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class AnalyticsModule { + @Binds + @Singleton + abstract fun bindAnalyticsHelper(impl: FirebaseAnalyticsHelper): AnalyticsHelper + + companion object { + @Provides + @Singleton + fun provideFirebaseAnalytics( + @ApplicationContext context: Context, + ): FirebaseAnalytics = FirebaseAnalytics.getInstance(context) + } +} From 32e0d49d54897377d95d9f07c21595485d91013e Mon Sep 17 00:00:00 2001 From: oilbeaneda <20221444@sungshin.ac.kr> Date: Wed, 22 Jul 2026 11:15:13 +0900 Subject: [PATCH 3/3] =?UTF-8?q?[setting/#58]=20=EC=BB=A4=EC=8A=A4=ED=85=80?= =?UTF-8?q?=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=207=EA=B0=9C=20=EB=A1=9C?= =?UTF-8?q?=EA=B9=85(=ED=8E=8D=EC=83=81=EC=84=B8/=EC=98=A8=EB=B3=B4?= =?UTF-8?q?=EB=94=A9/=ED=95=84=ED=84=B0/=EA=B2=80=EC=83=89/=EC=A0=9C?= =?UTF-8?q?=EB=B3=B4/=EC=B0=9C/=EC=A7=80=EB=8F=84=EB=A7=81=ED=81=AC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/presentation/home/HomeViewModel.kt | 27 +++++++++++++++++++ .../home/homesearch/HomeSearchViewModel.kt | 8 ++++++ .../mypage/report/ReportViewModel.kt | 8 ++++++ .../onboarding/signup/SignUpViewModel.kt | 8 ++++++ 4 files changed, 51 insertions(+) diff --git a/app/src/main/java/org/app/presentation/home/HomeViewModel.kt b/app/src/main/java/org/app/presentation/home/HomeViewModel.kt index 6c52093..b02b61d 100644 --- a/app/src/main/java/org/app/presentation/home/HomeViewModel.kt +++ b/app/src/main/java/org/app/presentation/home/HomeViewModel.kt @@ -8,6 +8,9 @@ import kotlinx.collections.immutable.toImmutableMap import kotlinx.collections.immutable.toImmutableSet import kotlinx.coroutines.Job import kotlinx.coroutines.launch +import org.app.core.analytics.AnalyticsEvent +import org.app.core.analytics.AnalyticsHelper +import org.app.core.analytics.AnalyticsParam import org.app.core.common.base.BaseViewModel import org.app.core.network.isHttpConflict import org.app.data.mapper.toPartialDetail @@ -49,6 +52,7 @@ class HomeViewModel private val userRepository: UserRepository, private val pubRepository: PubRepository, private val favoriteRepository: FavoriteRepository, + private val analyticsHelper: AnalyticsHelper, ) : BaseViewModel( HomeContract.State(), ) { @@ -158,6 +162,13 @@ class HomeViewModel foodCodes = event.foodCodes, ) setState { copy(filter = newFilter, showFilterBottomSheet = false) } + analyticsHelper.logEvent( + AnalyticsEvent.FILTER_APPLY, + mapOf( + AnalyticsParam.TEAM_COUNT to event.teamIds.size, + AnalyticsParam.REGION_COUNT to event.regions.size, + ), + ) // 선택된 지역들의 좌표를 모아 바운즈 이동 처리 // "서울 전체"(SEOUL_ALL)는 특정 지점이 아니라 전체 조회이므로 카메라 이동 대상에서 제외 @@ -174,6 +185,10 @@ class HomeViewModel } is HomeContract.Event.OnKakaoMapClick -> { + analyticsHelper.logEvent( + AnalyticsEvent.MAP_LINK_CLICK, + mapOf(AnalyticsParam.MAP_TYPE to "kakao"), + ) val kakaoFallback = "https://map.kakao.com/link/map/${event.name},${event.lat},${event.lng}" postSideEffect( @@ -186,6 +201,10 @@ class HomeViewModel } is HomeContract.Event.OnNaverMapClick -> { + analyticsHelper.logEvent( + AnalyticsEvent.MAP_LINK_CLICK, + mapOf(AnalyticsParam.MAP_TYPE to "naver"), + ) val naverUrl = "$NAVER_MAP_SCHEME://place?lat=${event.lat}&lng=${event.lng}" + "&name=${event.name}&appname=$NAVER_MAP_APP_NAME" @@ -332,6 +351,10 @@ class HomeViewModel selectedPubDetail = detail.copy(isFavoriteed = isFavoriteed), ) } + analyticsHelper.logEvent( + AnalyticsEvent.PUB_DETAIL_VIEW, + mapOf(AnalyticsParam.PUB_ID to pubId), + ) }.onFailure { error -> Timber.e("펍 상세 로드 실패: $error") setState { @@ -409,6 +432,10 @@ class HomeViewModel updateSelectedDetail = updateSelectedDetail, newFavoriteId = newId, ) + analyticsHelper.logEvent( + AnalyticsEvent.FAVORITE_ADD, + mapOf(AnalyticsParam.PUB_ID to pubId), + ) postSideEffect(HomeContract.SideEffect.ShowToast("즐겨찾기에 등록되었습니다.")) }.onFailure { error -> handleFavoriteFailure(error, "즐겨찾기 등록에 실패했습니다.") diff --git a/app/src/main/java/org/app/presentation/home/homesearch/HomeSearchViewModel.kt b/app/src/main/java/org/app/presentation/home/homesearch/HomeSearchViewModel.kt index 8a180bc..06d4bca 100644 --- a/app/src/main/java/org/app/presentation/home/homesearch/HomeSearchViewModel.kt +++ b/app/src/main/java/org/app/presentation/home/homesearch/HomeSearchViewModel.kt @@ -9,6 +9,9 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch +import org.app.core.analytics.AnalyticsEvent +import org.app.core.analytics.AnalyticsHelper +import org.app.core.analytics.AnalyticsParam import org.app.core.common.base.BaseViewModel import org.app.data.repository.api.PubRepository import org.app.presentation.home.model.PubSearchResult @@ -21,6 +24,7 @@ class HomeSearchViewModel @Inject constructor( private val pubRepository: PubRepository, + private val analyticsHelper: AnalyticsHelper, ) : BaseViewModel( HomeSearchContract.State(), ) { @@ -70,6 +74,10 @@ class HomeSearchViewModel setState { copy(isLoading = false, results = results.toImmutableList(), isEmpty = results.isEmpty()) } + analyticsHelper.logEvent( + AnalyticsEvent.MAP_SEARCH, + mapOf(AnalyticsParam.RESULT_COUNT to results.size), + ) }.onFailure { error -> Timber.e("검색 실패: $error") setState { copy(isLoading = false, results = persistentListOf(), isEmpty = true) } diff --git a/app/src/main/java/org/app/presentation/mypage/report/ReportViewModel.kt b/app/src/main/java/org/app/presentation/mypage/report/ReportViewModel.kt index 0b9f97d..e77058b 100644 --- a/app/src/main/java/org/app/presentation/mypage/report/ReportViewModel.kt +++ b/app/src/main/java/org/app/presentation/mypage/report/ReportViewModel.kt @@ -5,6 +5,9 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.launch +import org.app.core.analytics.AnalyticsEvent +import org.app.core.analytics.AnalyticsHelper +import org.app.core.analytics.AnalyticsParam import org.app.core.common.base.BaseViewModel import org.app.data.repository.api.ReportRepository import timber.log.Timber @@ -16,6 +19,7 @@ class ReportViewModel constructor( savedStateHandle: SavedStateHandle, private val reportRepository: ReportRepository, + private val analyticsHelper: AnalyticsHelper, ) : BaseViewModel( ReportContract.State(pubId = savedStateHandle.get("pubId")), ) { @@ -65,6 +69,10 @@ class ReportViewModel imageUris = s.imageUris, ).onSuccess { Timber.d("제보 완료: reportId=${it.reportId}") + analyticsHelper.logEvent( + AnalyticsEvent.REPORT_SUBMIT, + mapOf(AnalyticsParam.CATEGORY to s.selectedCategory.apiValue), + ) postSideEffect(ReportContract.SideEffect.ShowSuccessDialog) }.onFailure { error -> Timber.e(error, "제보 전송 실패") diff --git a/app/src/main/java/org/app/presentation/onboarding/signup/SignUpViewModel.kt b/app/src/main/java/org/app/presentation/onboarding/signup/SignUpViewModel.kt index 5139e66..260b89c 100644 --- a/app/src/main/java/org/app/presentation/onboarding/signup/SignUpViewModel.kt +++ b/app/src/main/java/org/app/presentation/onboarding/signup/SignUpViewModel.kt @@ -5,6 +5,9 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.toImmutableSet import kotlinx.coroutines.launch +import org.app.core.analytics.AnalyticsEvent +import org.app.core.analytics.AnalyticsHelper +import org.app.core.analytics.AnalyticsParam import org.app.core.common.base.BaseViewModel import org.app.data.repository.api.UserRepository import javax.inject.Inject @@ -14,6 +17,7 @@ class SignUpViewModel @Inject constructor( private val userRepository: UserRepository, + private val analyticsHelper: AnalyticsHelper, savedStateHandle: SavedStateHandle, ) : BaseViewModel( SignUpContract.State(nickname = savedStateHandle.get("nickname") ?: ""), @@ -80,6 +84,10 @@ class SignUpViewModel .postOnboarding(nickname = nickname, teamIds = teamIds) .onSuccess { setState { copy(isLoading = false) } + analyticsHelper.logEvent( + AnalyticsEvent.ONBOARDING_COMPLETE, + mapOf(AnalyticsParam.TEAM_COUNT to teamIds.size), + ) postSideEffect(SignUpContract.SideEffect.NavigateToComplete) }.onFailure { error -> setState { copy(isLoading = false) }