diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
index f40df71..8aa920c 100644
--- a/.github/workflows/android.yml
+++ b/.github/workflows/android.yml
@@ -1,28 +1,89 @@
name: Android CI
on:
- push:
- branches: [ "master" ]
pull_request:
branches: [ "master" ]
+# 전체 워크플로우에 대한 권한 설정
+permissions:
+ contents: write
+
+# 워크플로우 최적화
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
build:
-
runs-on: ubuntu-latest
+
+ # 빌드 캐시 설정
+ env:
+ GRADLE_OPTS: -Dorg.gradle.daemon=false -Dorg.gradle.parallel=true -Dorg.gradle.configureondemand=true -Dorg.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError
steps:
- uses: actions/checkout@v4
- - name: set up JDK 11
+
+ - name: Set up JDK 17
uses: actions/setup-java@v4
with:
- java-version: '11'
+ java-version: '17'
distribution: 'temurin'
cache: gradle
+
+ - name: Setup Gradle Cache
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.gradle/caches
+ ~/.gradle/wrapper
+ key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
+ restore-keys: |
+ ${{ runner.os }}-gradle-
+
+ - name: Setup Android SDK Cache
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.android/build-cache
+ ~/.android/cache
+ key: ${{ runner.os }}-android-${{ hashFiles('**/gradle.properties', '**/local.properties') }}
+ restore-keys: |
+ ${{ runner.os }}-android-
- name: Grant execute permission for gradlew
run: chmod +x gradlew
+ - name: Set Gradle Memory Settings
+ run: |
+ cat > gradle.properties << 'EOF'
+ # Project-wide Gradle settings.
+ # IDE (e.g. Android Studio) users:
+ # Gradle settings configured through the IDE *will override*
+ # any settings specified in this file.
+ # For more details on how to configure your build environment visit
+ # http://www.gradle.org/docs/current/userguide/build_environment.html
+ # Specifies the JVM arguments used for the daemon process.
+ # The setting is particularly useful for tweaking memory settings.
+ org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8
+ # When configured, Gradle will run in incubating parallel mode.
+ # This option should only be used with decoupled projects. For more details, visit
+ # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
+ # org.gradle.parallel=true
+ # AndroidX package structure to make it clearer which packages are bundled with the
+ # Android operating system, and which are packaged with your app's APK
+ # https://developer.android.com/topic/libraries/support-library/androidx-rn
+ android.useAndroidX=true
+ # Kotlin code style for this project: "official" or "obsolete":
+ kotlin.code.style=official
+ # Enables namespacing of each library's R class so that its R class includes only the
+ # resources declared in the library itself and none from the library's dependencies,
+ # thereby reducing the size of the R class for that library
+ android.nonTransitiveRClass=true
+ EOF
+ echo "Gradle properties file created with memory settings:"
+ cat gradle.properties
+
- name: Add Local Properties
env:
BASE_URL: ${{secrets.BASE_URL}}
@@ -32,18 +93,26 @@ jobs:
SIGNING_KEY_ALIAS: ${{secrets.SIGNING_KEY_ALIAS}}
SIGNING_KEY_PASSWORD: ${{secrets.SIGNING_KEY_PASSWORD}}
run: |
- echo "BASE_URL=$BASE_URL" >> ./local.properties
- echo "GOOGLE_API_KEY=$GOOGLE_API_KEY" >> ./local.properties
- echo "KAKAO_API_KEY=$KAKAO_API_KEY" >> ./local.properties
- echo "SIGNING_STORE_PASSWORD=$SIGNING_STORE_PASSWORD" >> ./local.properties
- echo "SIGNING_KEY_ALIAS=$SIGNING_KEY_ALIAS" >> ./local.properties
- echo "SIGNING_KEY_PASSWORD=$SIGNING_KEY_PASSWORD" >> ./local.properties
+ echo "DEBUG: BASE_URL from secrets = '$BASE_URL'"
+ cat > local.properties << EOF
+ BASE_URL=$BASE_URL
+ GOOGLE_API_KEY=$GOOGLE_API_KEY
+ KAKAO_API_KEY=$KAKAO_API_KEY
+ SIGNING_STORE_PASSWORD=$SIGNING_STORE_PASSWORD
+ SIGNING_KEY_ALIAS=$SIGNING_KEY_ALIAS
+ SIGNING_KEY_PASSWORD=$SIGNING_KEY_PASSWORD
+ EOF
+ echo "Created local.properties at project root:"
+ ls -la local.properties
+ cat local.properties
- name: Get Google Services JSON
env:
GOOGLE_SERVICES_JSON: ${{secrets.GOOGLE_SERVICES_JSON}}
- run:
- echo '$GOOGLE_SERVICES_JSON' > ./app/google-services.json
+ run: |
+ echo '${{ secrets.GOOGLE_SERVICES_JSON }}' > ./app/google-services.json
+ echo "Google Services JSON file created successfully"
+ ls -la ./app/google-services.json
- name: Create Keystore File
env:
@@ -51,11 +120,22 @@ jobs:
run: |
echo $KEYSTORE_BASE64 | base64 -d > ./app/runcombi-keystore.jks
- - name: Build with Gradle
- run: ./gradlew build
-
- - name: Build Release APK
- run: ./gradlew assembleRelease
+ - name: Check if runcombi-keystore.jks exists
+ run: |
+ if [ -f "./app/runcombi-keystore.jks" ]; then
+ echo "✅ runcombi-keystore.jks file exists in the app directory."
+ ls -la ./app/runcombi-keystore.jks
+ else
+ echo "❌ Error: runcombi-keystore.jks file is missing in the app directory." >&2
+ exit 1
+ fi
+
+ - name: List keys in runcombi-keystore.jks
+ env:
+ SIGNING_STORE_PASSWORD: ${{secrets.SIGNING_STORE_PASSWORD}}
+ run: |
+ echo "Checking keystore contents..."
+ keytool -list -v -keystore ./app/runcombi-keystore.jks -storepass "$SIGNING_STORE_PASSWORD" || echo "Failed to list keystore contents"
- name: Extract App Version
id: app_version
@@ -64,12 +144,46 @@ jobs:
VERSION_CODE=$(grep 'versionCode' app/build.gradle.kts | sed 's/.*versionCode = \([0-9]*\)/\1/')
echo "version_name=$VERSION_NAME" >> $GITHUB_OUTPUT
echo "version_code=$VERSION_CODE" >> $GITHUB_OUTPUT
+ echo "Extracted version: $VERSION_NAME ($VERSION_CODE)"
+
+
+
+ - name: Build with Gradle
+ run: ./gradlew build --parallel --max-workers=2 --daemon
+
+ - name: Build Release APK
+ run: |
+ echo "Starting assembleRelease..."
+ ./gradlew assembleRelease --parallel --max-workers=2 --daemon --info --stacktrace
+ echo "assembleRelease completed with exit code: $?"
+ echo "Checking if APK was generated..."
+ if [ -f "app/build/outputs/apk/release/app-release.apk" ]; then
+ echo "✅ APK file found!"
+ ls -la app/build/outputs/apk/release/
+ else
+ echo "❌ APK file not found!"
+ echo "Checking build directory..."
+ ls -la app/build/outputs/ || echo "Outputs directory not found"
+ echo "Checking for any APK files..."
+ find app/build/outputs/ -name "*.apk" -type f || echo "No APK files found"
+ fi
+
+ - name: Check APK output
+ run: |
+ echo "Checking APK output directory..."
+ ls -la app/build/outputs/apk/release/ || echo "Release directory not found"
+ echo "Checking all APK outputs..."
+ find app/build/outputs/ -name "*.apk" -type f || echo "No APK files found"
+ echo "Checking build directory structure..."
+ ls -la app/build/outputs/ || echo "Outputs directory not found"
- name: Upload Release Build to Artifacts
uses: actions/upload-artifact@v4
with:
name: release-artifacts
- path: app/build/outputs/apk/release/
+ path: |
+ app/build/outputs/apk/mock/release/
+ app/build/outputs/apk/prod/release/
if-no-files-found: error
- name: Create Github Release
@@ -79,7 +193,8 @@ jobs:
release_name: RunCombi Android v${{ steps.app_version.outputs.version_name }}
generate_release_notes: true
files: |
- app/build/outputs/apk/release/app-release.apk
+ app/build/outputs/apk/mock/release/app-mock-release.apk
+ app/build/outputs/apk/prod/release/app-prod-release.apk
body: |
## RunCombi Android v${{ steps.app_version.outputs.version_name }}
@@ -90,6 +205,10 @@ jobs:
- PR: ${{ github.event.pull_request.title }}
- Author: @${{ github.event.pull_request.user.login }}
- Branch: ${{ github.event.pull_request.head.ref }} → ${{ github.event.pull_request.base.ref }}
+
+ ### APK Files
+ - **Mock Release**: app-mock-release.apk
+ - **Prod Release**: app-prod-release.apk
- name: Upload artifact to Firebase App Distribution
uses: wzieba/Firebase-Distribution-Github-Action@v1
@@ -97,23 +216,15 @@ jobs:
appId: ${{secrets.FIREBASE_APP_ID}}
serviceCredentialsFileContent: ${{ secrets.CREDENTIAL_FILE_CONTENT }}
groups: testers
- file: app/build/outputs/apk/release/app-release.apk
+ file: app/build/outputs/apk/prod/release/app-prod-release.apk
releaseNotes: |
RunCombi Android v${{ steps.app_version.outputs.version_name }}
PR: ${{ github.event.pull_request.title }}
Author: @${{ github.event.pull_request.user.login }}
Branch: ${{ github.event.pull_request.head.ref }} → ${{ github.event.pull_request.base.ref }}
-
- - name: Upload APK to Slack
- if: ${{success()}}
- uses: 8398a7/action-slack@v3
- with:
- status: success
- webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
- channel: '#general'
- text: 'RunCombi Android APK 빌드 완료! 🎉'
- file: app/build/outputs/apk/release/app-release.apk
+
+ APK: Prod Release Version
- name: If Success, Send notification on Slack
if: ${{success()}}
@@ -121,18 +232,15 @@ jobs:
env:
SLACK_COLOR: '#60E0C5'
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}
- SLACK_TITLE: 'RunCombi Android PR Check Success ✅'
+ SLACK_TITLE: 'RunCombi Android 빌드 성공 ✅'
MSG_MINIMAL: true
- SLACK_USERNAME: RunCombi Android
- SLACK_MESSAGE: 'RunCombi Android PR 체크 성공 🎉 (v${{ steps.app_version.outputs.version_name }} - ${{ steps.app_version.outputs.version_code }})%0A%0A**PR 제목:** ${{ github.event.pull_request.title }}%0A**PR 설명:** ${{ github.event.pull_request.body }}%0A**작성자:** @${{ github.event.pull_request.user.login }}%0A**브랜치:** ${{ github.event.pull_request.head.ref }} → ${{ github.event.pull_request.base.ref }}'
+ SLACK_USERNAME: RunCombi Android CI
+ SLACK_MESSAGE: |
+ 🎉 RunCombi Android 빌드 성공!
+
+ 📱 **앱 버전**: v${{ steps.app_version.outputs.version_name }} (${{ steps.app_version.outputs.version_code }})
+ 🔗 **PR 제목**: ${{ github.event.pull_request.title }}
+ 📝 **PR 내용**: ${{ github.event.pull_request.body || '내용이 없습니다.' }}
+ 🚀 **GitHub Release**: https://github.com/${{ github.repository }}/releases/tag/v${{ steps.app_version.outputs.version_name }}
+
- - name: If Fail, Send notification on Slack
- if: ${{failure()}}
- uses: rtCamp/action-slack-notify@v2
- env:
- SLACK_COLOR: '#ff0000'
- SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}
- SLACK_TITLE: 'RunCombi Android PR Check Failed ❌'
- MSG_MINIMAL: true
- SLACK_USERNAME: RunCombi Android
- SLACK_MESSAGE: 'RunCombi Android PR 체크 실패 - 확인이 필요합니다 🔍 (v${{ steps.app_version.outputs.version_name }} - ${{ steps.app_version.outputs.version_code }})%0A%0A**PR 제목:** ${{ github.event.pull_request.title }}%0A**PR 설명:** ${{ github.event.pull_request.body }}%0A**작성자:** @${{ github.event.pull_request.user.login }}%0A**브랜치:** ${{ github.event.pull_request.head.ref }} → ${{ github.event.pull_request.base.ref }}'
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 6b0007d..b3307b6 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -2,6 +2,8 @@ import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
plugins {
id("runcombi.android.application")
+ alias(libs.plugins.google.services)
+ alias(libs.plugins.firebase.crashlytics)
}
android {
@@ -43,10 +45,17 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
-
isDebuggable = false
}
+ debug {
+ isDebuggable = true
+ }
+ }
+
+ buildFeatures {
+ buildConfig = true
}
+
flavorDimensions += "mode"
productFlavors {
create("mock") {
@@ -63,6 +72,11 @@ dependencies {
implementation(libs.hilt.android)
implementation(libs.v2.user)
+ // Firebase
+ implementation(platform(libs.firebase.bom))
+ implementation(libs.firebase.analytics)
+ implementation(libs.firebase.crashlytics)
+
implementation(project(":feature:main"))
implementation(project(":feature:login"))
implementation(project(":feature:history"))
@@ -77,4 +91,10 @@ dependencies {
implementation(project(":core:data:walk"))
implementation(project(":core:data:history"))
implementation(project(":core:data:setting"))
+
+ // Test dependencies
+ testImplementation(libs.junit)
+ testImplementation(libs.kotlin.test)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
}
\ No newline at end of file
diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts
index 2490982..15c63d7 100644
--- a/build-logic/settings.gradle.kts
+++ b/build-logic/settings.gradle.kts
@@ -1,3 +1,5 @@
+rootProject.name = "build-logic"
+
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
@Suppress("UnstableApiUsage")
dependencyResolutionManagement {
diff --git a/build-logic/src/main/java/runcombi.android.feature.gradle.kts b/build-logic/src/main/java/runcombi.android.feature.gradle.kts
index 056eb6f..120ca2f 100644
--- a/build-logic/src/main/java/runcombi.android.feature.gradle.kts
+++ b/build-logic/src/main/java/runcombi.android.feature.gradle.kts
@@ -23,6 +23,7 @@ configureHiltAndroid()
dependencies {
implementation(project(":core:designsystem"))
implementation(project(":core:navigation"))
+ implementation(project(":core:analytics"))
implementation(project(":core:ui"))
implementation(project(":core:domain:auth"))
implementation(project(":core:domain:common"))
diff --git a/build.gradle.kts b/build.gradle.kts
index e2fedcc..3a18517 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -9,4 +9,6 @@ plugins {
alias(libs.plugins.kotlin.plugin.serialization) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.jetbrains.kotlin.jvm) apply false
+ alias(libs.plugins.google.services) apply false
+ alias(libs.plugins.firebase.crashlytics) apply false
}
\ No newline at end of file
diff --git a/core/analytics/.gitignore b/core/analytics/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/core/analytics/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/core/analytics/README.md b/core/analytics/README.md
new file mode 100644
index 0000000..d8c3a13
--- /dev/null
+++ b/core/analytics/README.md
@@ -0,0 +1,92 @@
+# Analytics 모듈
+
+RunCombi_Android 프로젝트의 분석 이벤트 로깅을 위한 모듈입니다.
+
+## 주요 구성 요소
+
+### 1. AnalyticsEvent
+분석 이벤트를 정의하는 데이터 클래스입니다.
+
+### 2. AnalyticsHelper
+분석 이벤트를 로깅하는 인터페이스입니다.
+
+### 3. StubAnalyticsHelper
+개발 및 디버그 빌드에서 사용하는 구현체로, 이벤트를 logcat에 기록합니다.
+
+### 4. FirebaseAnalyticsHelper
+프로덕션 빌드에서 사용하는 구현체로, Firebase Analytics에 실제 이벤트를 전송합니다.
+
+### 5. NoOpAnalyticsHelper
+테스트와 프리뷰에서 사용하는 구현체로, 아무것도 하지 않습니다.
+
+## 사용법
+
+### 기본 사용법
+
+```kotlin
+@Inject
+lateinit var analyticsHelper: AnalyticsHelper
+
+analyticsHelper.logEvent(
+ AnalyticsEvent(
+ type = "custom_event",
+ extras = listOf(
+ AnalyticsEvent.Param(key = "key", value = "value")
+ )
+ )
+)
+```
+
+### 확장 함수 사용
+
+```kotlin
+analyticsHelper.logScreenView("LoginActivity")
+analyticsHelper.logUserLogin("google")
+analyticsHelper.logWalkStarted("outdoor")
+analyticsHelper.logSettingChanged("theme", "dark")
+analyticsHelper.logFeatureUsed("camera")
+analyticsHelper.logButtonClick("login_button", "LoginScreen")
+analyticsHelper.logError("network_error", "Connection failed", "MainScreen")
+```
+
+### Compose에서 사용
+
+```kotlin
+val analyticsHelper = LocalAnalyticsHelper.current
+
+LaunchedEffect(Unit) {
+ analyticsHelper.logScreenView("HomeScreen")
+}
+```
+
+## 표준 이벤트 타입
+
+- `screen_view`: 화면 보기
+- `user_login`: 사용자 로그인
+- `user_signup`: 사용자 회원가입
+- `walk_started`: 산책 시작
+- `walk_completed`: 산책 완료
+- `setting_changed`: 설정 변경
+- `feature_used`: 기능 사용
+- `button_click`: 버튼 클릭
+- `error_occurred`: 오류 발생
+- `app_started`: 앱 시작
+- `app_backgrounded`: 앱 백그라운드
+- `app_foregrounded`: 앱 포그라운드
+
+## 표준 매개변수 키
+
+- `screen_name`: 화면 이름
+- `login_method`: 로그인 방법
+- `signup_method`: 회원가입 방법
+- `walk_type`: 산책 타입
+- `walk_duration`: 산책 시간
+- `walk_distance`: 산책 거리
+- `setting_name`: 설정 이름
+- `setting_value`: 설정 값
+- `feature_name`: 기능 이름
+- `button_name`: 버튼 이름
+- `error_type`: 오류 타입
+- `error_message`: 오류 메시지
+- `is_new_user`: 신규 사용자 여부
+- `user_status`: 사용자 상태
diff --git a/core/analytics/build.gradle.kts b/core/analytics/build.gradle.kts
new file mode 100644
index 0000000..9de162c
--- /dev/null
+++ b/core/analytics/build.gradle.kts
@@ -0,0 +1,45 @@
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.hilt.android)
+ alias(libs.plugins.kotlin.ksp)
+}
+
+android {
+ namespace = "com.combo.runcombi.analytics"
+ compileSdk = 35
+
+ defaultConfig {
+ minSdk = 26
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+}
+
+dependencies {
+ // Hilt
+ implementation(libs.hilt.android)
+ ksp(libs.hilt.compiler)
+
+ // Firebase
+ implementation(platform(libs.firebase.bom))
+ implementation(libs.firebase.analytics)
+}
diff --git a/core/analytics/src/main/java/com/combo/runcombi/analytics/AnalyticsEvent.kt b/core/analytics/src/main/java/com/combo/runcombi/analytics/AnalyticsEvent.kt
new file mode 100644
index 0000000..a53bfdf
--- /dev/null
+++ b/core/analytics/src/main/java/com/combo/runcombi/analytics/AnalyticsEvent.kt
@@ -0,0 +1,44 @@
+package com.combo.runcombi.analytics
+
+data class AnalyticsEvent(
+ val type: String,
+ val extras: List = emptyList(),
+) {
+ class Types {
+ companion object {
+ const val SCREEN_VIEW = "screen_view"
+ const val USER_LOGIN = "user_login"
+ const val USER_SIGNUP = "user_signup"
+ const val WALK_STARTED = "walk_started"
+ const val WALK_COMPLETED = "walk_completed"
+ const val SETTING_CHANGED = "setting_changed"
+ const val FEATURE_USED = "feature_used"
+ const val BUTTON_CLICK = "button_click"
+ const val ERROR_OCCURRED = "error_occurred"
+ const val APP_STARTED = "app_started"
+ const val APP_BACKGROUNDED = "app_backgrounded"
+ const val APP_FOREGROUNDED = "app_foregrounded"
+ }
+ }
+
+ data class Param(val key: String, val value: String)
+
+ class ParamKeys {
+ companion object {
+ const val SCREEN_NAME = "screen_name"
+ const val LOGIN_METHOD = "login_method"
+ const val SIGNUP_METHOD = "signup_method"
+ const val WALK_TYPE = "walk_type"
+ const val WALK_DURATION = "walk_duration"
+ const val WALK_DISTANCE = "walk_distance"
+ const val SETTING_NAME = "setting_name"
+ const val SETTING_VALUE = "setting_value"
+ const val FEATURE_NAME = "feature_name"
+ const val BUTTON_NAME = "button_name"
+ const val ERROR_TYPE = "error_type"
+ const val ERROR_MESSAGE = "error_message"
+ const val IS_NEW_USER = "is_new_user"
+ const val USER_STATUS = "user_status"
+ }
+ }
+}
diff --git a/core/analytics/src/main/java/com/combo/runcombi/analytics/AnalyticsExtensions.kt b/core/analytics/src/main/java/com/combo/runcombi/analytics/AnalyticsExtensions.kt
new file mode 100644
index 0000000..66a0ffc
--- /dev/null
+++ b/core/analytics/src/main/java/com/combo/runcombi/analytics/AnalyticsExtensions.kt
@@ -0,0 +1,141 @@
+package com.combo.runcombi.analytics
+
+import com.combo.runcombi.analytics.AnalyticsEvent
+import com.combo.runcombi.analytics.AnalyticsEvent.Param
+import com.combo.runcombi.analytics.AnalyticsHelper
+
+// 기본 화면 및 사용자 이벤트
+fun AnalyticsHelper.logScreenView(screenName: String) =
+ logEvent(
+ AnalyticsEvent(
+ type = AnalyticsEvent.Types.SCREEN_VIEW,
+ extras = listOf(
+ Param(key = AnalyticsEvent.ParamKeys.SCREEN_NAME, value = screenName),
+ ),
+ ),
+ )
+
+fun AnalyticsHelper.logUserLogin(loginMethod: String) =
+ logEvent(
+ AnalyticsEvent(
+ type = AnalyticsEvent.Types.USER_LOGIN,
+ extras = listOf(
+ Param(key = AnalyticsEvent.ParamKeys.LOGIN_METHOD, value = loginMethod),
+ ),
+ ),
+ )
+
+fun AnalyticsHelper.logUserSignup(signupMethod: String) =
+ logEvent(
+ AnalyticsEvent(
+ type = AnalyticsEvent.Types.USER_SIGNUP,
+ extras = listOf(
+ Param(key = AnalyticsEvent.ParamKeys.SIGNUP_METHOD, value = signupMethod),
+ ),
+ ),
+ )
+
+// 산책 관련 이벤트
+fun AnalyticsHelper.logWalkStarted(walkType: String) =
+ logEvent(
+ AnalyticsEvent(
+ type = AnalyticsEvent.Types.WALK_STARTED,
+ extras = listOf(
+ Param(key = AnalyticsEvent.ParamKeys.WALK_TYPE, value = walkType),
+ ),
+ ),
+ )
+
+fun AnalyticsHelper.logWalkCompleted(duration: String, distance: String) =
+ logEvent(
+ AnalyticsEvent(
+ type = AnalyticsEvent.Types.WALK_COMPLETED,
+ extras = listOf(
+ Param(key = AnalyticsEvent.ParamKeys.WALK_DURATION, value = duration),
+ Param(key = AnalyticsEvent.ParamKeys.WALK_DISTANCE, value = distance),
+ ),
+ ),
+ )
+
+// 설정 및 기능 이벤트
+fun AnalyticsHelper.logSettingChanged(settingName: String, settingValue: String) =
+ logEvent(
+ AnalyticsEvent(
+ type = AnalyticsEvent.Types.SETTING_CHANGED,
+ extras = listOf(
+ Param(key = AnalyticsEvent.ParamKeys.SETTING_NAME, value = settingName),
+ Param(key = AnalyticsEvent.ParamKeys.SETTING_VALUE, value = settingValue),
+ ),
+ ),
+ )
+
+fun AnalyticsHelper.logFeatureUsed(featureName: String) =
+ logEvent(
+ AnalyticsEvent(
+ type = AnalyticsEvent.Types.FEATURE_USED,
+ extras = listOf(
+ Param(key = AnalyticsEvent.ParamKeys.FEATURE_NAME, value = featureName),
+ ),
+ ),
+ )
+
+// 앱 생명주기 이벤트
+fun AnalyticsHelper.logAppStarted(isNewUser: Boolean, userStatus: String) =
+ logEvent(
+ AnalyticsEvent(
+ type = AnalyticsEvent.Types.APP_STARTED,
+ extras = listOf(
+ Param(key = AnalyticsEvent.ParamKeys.IS_NEW_USER, value = isNewUser.toString()),
+ Param(key = AnalyticsEvent.ParamKeys.USER_STATUS, value = userStatus),
+ ),
+ ),
+ )
+
+fun AnalyticsHelper.logAppBackgrounded() =
+ logEvent(
+ AnalyticsEvent(
+ type = AnalyticsEvent.Types.APP_BACKGROUNDED
+ )
+ )
+
+fun AnalyticsHelper.logAppForegrounded() =
+ logEvent(
+ AnalyticsEvent(
+ type = AnalyticsEvent.Types.APP_FOREGROUNDED
+ )
+ )
+
+// 사용자 상호작용 이벤트
+fun AnalyticsHelper.logButtonClick(buttonName: String, screenName: String? = null) =
+ logEvent(
+ AnalyticsEvent(
+ type = AnalyticsEvent.Types.BUTTON_CLICK,
+ extras = listOf(
+ Param(key = AnalyticsEvent.ParamKeys.BUTTON_NAME, value = buttonName),
+ ).let { params ->
+ if (screenName != null) {
+ params + Param(key = AnalyticsEvent.ParamKeys.SCREEN_NAME, value = screenName)
+ } else {
+ params
+ }
+ }
+ )
+ )
+
+// 오류 및 예외 이벤트
+fun AnalyticsHelper.logError(errorType: String, errorMessage: String, screenName: String? = null) =
+ logEvent(
+ AnalyticsEvent(
+ type = AnalyticsEvent.Types.ERROR_OCCURRED,
+ extras = listOf(
+ Param(key = AnalyticsEvent.ParamKeys.ERROR_TYPE, value = errorType),
+ Param(key = AnalyticsEvent.ParamKeys.ERROR_MESSAGE, value = errorMessage),
+ ).let { params ->
+ if (screenName != null) {
+ params + Param(key = AnalyticsEvent.ParamKeys.SCREEN_NAME, value = screenName)
+ } else {
+ params
+ }
+ }
+ )
+ )
diff --git a/core/analytics/src/main/java/com/combo/runcombi/analytics/AnalyticsHelper.kt b/core/analytics/src/main/java/com/combo/runcombi/analytics/AnalyticsHelper.kt
new file mode 100644
index 0000000..75dab94
--- /dev/null
+++ b/core/analytics/src/main/java/com/combo/runcombi/analytics/AnalyticsHelper.kt
@@ -0,0 +1,5 @@
+package com.combo.runcombi.analytics
+
+interface AnalyticsHelper {
+ fun logEvent(event: AnalyticsEvent)
+}
diff --git a/core/analytics/src/main/java/com/combo/runcombi/analytics/AnalyticsModule.kt b/core/analytics/src/main/java/com/combo/runcombi/analytics/AnalyticsModule.kt
new file mode 100644
index 0000000..f1b4ccb
--- /dev/null
+++ b/core/analytics/src/main/java/com/combo/runcombi/analytics/AnalyticsModule.kt
@@ -0,0 +1,15 @@
+package com.combo.runcombi.analytics
+
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+
+@Module
+@InstallIn(SingletonComponent::class)
+internal abstract class AnalyticsModule {
+
+ @Binds
+ abstract fun bindsAnalyticsHelper(analyticsHelperImpl: FirebaseAnalyticsHelper): AnalyticsHelper
+
+}
diff --git a/core/analytics/src/main/java/com/combo/runcombi/analytics/FirebaseAnalyticsHelper.kt b/core/analytics/src/main/java/com/combo/runcombi/analytics/FirebaseAnalyticsHelper.kt
new file mode 100644
index 0000000..776f683
--- /dev/null
+++ b/core/analytics/src/main/java/com/combo/runcombi/analytics/FirebaseAnalyticsHelper.kt
@@ -0,0 +1,32 @@
+package com.combo.runcombi.analytics
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.Bundle
+import com.google.firebase.analytics.FirebaseAnalytics
+import dagger.hilt.android.qualifiers.ApplicationContext
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@SuppressLint("MissingPermission")
+@Singleton
+internal class FirebaseAnalyticsHelper @Inject constructor(
+ @ApplicationContext private val context: Context
+) : AnalyticsHelper {
+
+ private val firebaseAnalytics: FirebaseAnalytics by lazy {
+ FirebaseAnalytics.getInstance(context)
+ }
+
+ override fun logEvent(event: AnalyticsEvent) {
+ val bundle = Bundle().apply {
+ putString(FirebaseAnalytics.Param.ITEM_ID, event.type)
+
+ event.extras.forEach { param ->
+ putString(param.key, param.value)
+ }
+ }
+
+ firebaseAnalytics.logEvent(event.type, bundle)
+ }
+}
diff --git a/core/analytics/src/main/java/com/combo/runcombi/analytics/NoOpAnalyticsHelper.kt b/core/analytics/src/main/java/com/combo/runcombi/analytics/NoOpAnalyticsHelper.kt
new file mode 100644
index 0000000..9c82344
--- /dev/null
+++ b/core/analytics/src/main/java/com/combo/runcombi/analytics/NoOpAnalyticsHelper.kt
@@ -0,0 +1,5 @@
+package com.combo.runcombi.analytics
+
+class NoOpAnalyticsHelper : AnalyticsHelper {
+ override fun logEvent(event: AnalyticsEvent) = Unit
+}
diff --git a/core/analytics/src/main/java/com/combo/runcombi/analytics/StubAnalyticsHelper.kt b/core/analytics/src/main/java/com/combo/runcombi/analytics/StubAnalyticsHelper.kt
new file mode 100644
index 0000000..0a9e533
--- /dev/null
+++ b/core/analytics/src/main/java/com/combo/runcombi/analytics/StubAnalyticsHelper.kt
@@ -0,0 +1,18 @@
+package com.combo.runcombi.analytics
+
+import android.util.Log
+import javax.inject.Inject
+import javax.inject.Singleton
+
+private const val TAG = "Analytics"
+
+@Singleton
+internal class StubAnalyticsHelper @Inject constructor() : AnalyticsHelper {
+ override fun logEvent(event: AnalyticsEvent) {
+ val extrasStr = if (event.extras.isNotEmpty()) {
+ event.extras.joinToString(", ") { "${it.key}=${it.value}" }
+ } else ""
+
+ Log.d(TAG, "[${event.type}] $extrasStr")
+ }
+}
diff --git a/core/data/auth/src/androidTest/java/com/combo/auth/ExampleInstrumentedTest.kt b/core/data/auth/src/androidTest/java/com/combo/auth/ExampleInstrumentedTest.kt
index 296d24d..5c686a0 100644
--- a/core/data/auth/src/androidTest/java/com/combo/auth/ExampleInstrumentedTest.kt
+++ b/core/data/auth/src/androidTest/java/com/combo/auth/ExampleInstrumentedTest.kt
@@ -1,24 +1 @@
package com.combo.auth
-
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.ext.junit.runners.AndroidJUnit4
-
-import org.junit.Test
-import org.junit.runner.RunWith
-
-import org.junit.Assert.*
-
-/**
- * Instrumented test, which will execute on an Android device.
- *
- * See [testing documentation](http://d.android.com/tools/testing).
- */
-@RunWith(AndroidJUnit4::class)
-class ExampleInstrumentedTest {
- @Test
- fun useAppContext() {
- // Context of the app under test.
- val appContext = InstrumentationRegistry.getInstrumentation().targetContext
- assertEquals("com.combo.auth.test", appContext.packageName)
- }
-}
\ No newline at end of file
diff --git a/core/data/history/src/androidTest/java/com/combo/runcombi/history/ExampleInstrumentedTest.kt b/core/data/history/src/androidTest/java/com/combo/runcombi/history/ExampleInstrumentedTest.kt
index cc26751..715fbf8 100644
--- a/core/data/history/src/androidTest/java/com/combo/runcombi/history/ExampleInstrumentedTest.kt
+++ b/core/data/history/src/androidTest/java/com/combo/runcombi/history/ExampleInstrumentedTest.kt
@@ -1,24 +1 @@
package com.combo.runcombi.history
-
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.ext.junit.runners.AndroidJUnit4
-
-import org.junit.Test
-import org.junit.runner.RunWith
-
-import org.junit.Assert.*
-
-/**
- * Instrumented test, which will execute on an Android device.
- *
- * See [testing documentation](http://d.android.com/tools/testing).
- */
-@RunWith(AndroidJUnit4::class)
-class ExampleInstrumentedTest {
- @Test
- fun useAppContext() {
- // Context of the app under test.
- val appContext = InstrumentationRegistry.getInstrumentation().targetContext
- assertEquals("com.combo.runcombi.history.test", appContext.packageName)
- }
-}
\ No newline at end of file
diff --git a/core/data/setting/src/androidTest/java/com/combo/runcombi/setting/ExampleInstrumentedTest.kt b/core/data/setting/src/androidTest/java/com/combo/runcombi/setting/ExampleInstrumentedTest.kt
index 8c340ed..1d39ec4 100644
--- a/core/data/setting/src/androidTest/java/com/combo/runcombi/setting/ExampleInstrumentedTest.kt
+++ b/core/data/setting/src/androidTest/java/com/combo/runcombi/setting/ExampleInstrumentedTest.kt
@@ -1,24 +1 @@
package com.combo.runcombi.setting
-
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.ext.junit.runners.AndroidJUnit4
-
-import org.junit.Test
-import org.junit.runner.RunWith
-
-import org.junit.Assert.*
-
-/**
- * Instrumented test, which will execute on an Android device.
- *
- * See [testing documentation](http://d.android.com/tools/testing).
- */
-@RunWith(AndroidJUnit4::class)
-class ExampleInstrumentedTest {
- @Test
- fun useAppContext() {
- // Context of the app under test.
- val appContext = InstrumentationRegistry.getInstrumentation().targetContext
- assertEquals("com.combo.runcombi.setting.test", appContext.packageName)
- }
-}
\ No newline at end of file
diff --git a/core/data/walk/src/androidTest/java/com/combo/runcombi/walk/ExampleInstrumentedTest.kt b/core/data/walk/src/androidTest/java/com/combo/runcombi/walk/ExampleInstrumentedTest.kt
index 13bcc53..21c437d 100644
--- a/core/data/walk/src/androidTest/java/com/combo/runcombi/walk/ExampleInstrumentedTest.kt
+++ b/core/data/walk/src/androidTest/java/com/combo/runcombi/walk/ExampleInstrumentedTest.kt
@@ -1,24 +1,2 @@
package com.combo.runcombi.walk
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.ext.junit.runners.AndroidJUnit4
-
-import org.junit.Test
-import org.junit.runner.RunWith
-
-import org.junit.Assert.*
-
-/**
- * Instrumented test, which will execute on an Android device.
- *
- * See [testing documentation](http://d.android.com/tools/testing).
- */
-@RunWith(AndroidJUnit4::class)
-class ExampleInstrumentedTest {
- @Test
- fun useAppContext() {
- // Context of the app under test.
- val appContext = InstrumentationRegistry.getInstrumentation().targetContext
- assertEquals("com.combo.runcombi.walk.test", appContext.packageName)
- }
-}
\ No newline at end of file
diff --git a/core/datastore/src/androidTest/java/com/combo/runcombi/datastore/ExampleInstrumentedTest.kt b/core/datastore/src/androidTest/java/com/combo/runcombi/datastore/ExampleInstrumentedTest.kt
index be2072f..2472014 100644
--- a/core/datastore/src/androidTest/java/com/combo/runcombi/datastore/ExampleInstrumentedTest.kt
+++ b/core/datastore/src/androidTest/java/com/combo/runcombi/datastore/ExampleInstrumentedTest.kt
@@ -1,24 +1,2 @@
package com.combo.runcombi.datastore
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.ext.junit.runners.AndroidJUnit4
-
-import org.junit.Test
-import org.junit.runner.RunWith
-
-import org.junit.Assert.*
-
-/**
- * Instrumented test, which will execute on an Android device.
- *
- * See [testing documentation](http://d.android.com/tools/testing).
- */
-@RunWith(AndroidJUnit4::class)
-class ExampleInstrumentedTest {
- @Test
- fun useAppContext() {
- // Context of the app under test.
- val appContext = InstrumentationRegistry.getInstrumentation().targetContext
- assertEquals("com.combo.runcombi.datastore.test", appContext.packageName)
- }
-}
\ No newline at end of file
diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts
index a45f417..65426d1 100644
--- a/core/network/build.gradle.kts
+++ b/core/network/build.gradle.kts
@@ -33,7 +33,7 @@ android {
)
}
}
-
+
buildFeatures {
buildConfig = true
}
diff --git a/feature/login/src/main/java/com/combo/runcombi/feature/login/LoginScreen.kt b/feature/login/src/main/java/com/combo/runcombi/feature/login/LoginScreen.kt
index 1f805ce..64ce3c9 100644
--- a/feature/login/src/main/java/com/combo/runcombi/feature/login/LoginScreen.kt
+++ b/feature/login/src/main/java/com/combo/runcombi/feature/login/LoginScreen.kt
@@ -24,10 +24,12 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
+import com.combo.runcombi.analytics.logButtonClick
+import com.combo.runcombi.analytics.logScreenView
import com.combo.runcombi.core.designsystem.component.StableImage
-import com.combo.runcombi.ui.ext.screenDefaultPadding
import com.combo.runcombi.core.designsystem.theme.RunCombiTypography
import com.combo.runcombi.domain.user.model.MemberStatus
+import com.combo.runcombi.ui.ext.screenDefaultPadding
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
@@ -71,9 +73,16 @@ private fun rememberLoginManager(): LoginManager {
@Composable
fun LoginScreen(
- modifier: Modifier = Modifier,
onKakaoLoginClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ viewModel: LoginViewModel = hiltViewModel(),
) {
+ val analyticsHelper = viewModel.analyticsHelper
+
+ LaunchedEffect(Unit) {
+ analyticsHelper.logScreenView("LoginScreen")
+ }
+
Box(
modifier = modifier
.fillMaxSize()
@@ -92,7 +101,11 @@ fun LoginScreen(
.align(Alignment.BottomCenter)
) {
Button(
- onClick = onKakaoLoginClick,
+ onClick = {
+ // 카카오 로그인 버튼 클릭 이벤트 로깅
+ analyticsHelper.logButtonClick("kakao_login", "LoginScreen")
+ onKakaoLoginClick()
+ },
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFFEE102),
contentColor = Color.Black
diff --git a/feature/login/src/main/java/com/combo/runcombi/feature/login/LoginViewModel.kt b/feature/login/src/main/java/com/combo/runcombi/feature/login/LoginViewModel.kt
index 58e2d96..31fd686 100644
--- a/feature/login/src/main/java/com/combo/runcombi/feature/login/LoginViewModel.kt
+++ b/feature/login/src/main/java/com/combo/runcombi/feature/login/LoginViewModel.kt
@@ -2,6 +2,7 @@ package com.combo.runcombi.feature.login
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import com.combo.runcombi.analytics.AnalyticsHelper
import com.combo.runcombi.auth.usecase.LoginUseCase
import com.combo.runcombi.common.DomainResult
import com.combo.runcombi.domain.user.model.MemberStatus
@@ -18,7 +19,8 @@ import javax.inject.Inject
@HiltViewModel
class LoginViewModel @Inject constructor(
private val loginUseCase: LoginUseCase,
- private val getUserInfoUseCase: GetUserInfoUseCase
+ private val getUserInfoUseCase: GetUserInfoUseCase,
+ val analyticsHelper: AnalyticsHelper
) : ViewModel() {
private val _eventFlow = MutableSharedFlow()
val eventFlow = _eventFlow.asSharedFlow()
diff --git a/feature/setting/src/main/java/com/combo/runcombi/setting/screen/AnnouncementDetailScreen.kt b/feature/setting/src/main/java/com/combo/runcombi/setting/screen/AnnouncementDetailScreen.kt
index b24d392..41adbd2 100644
--- a/feature/setting/src/main/java/com/combo/runcombi/setting/screen/AnnouncementDetailScreen.kt
+++ b/feature/setting/src/main/java/com/combo/runcombi/setting/screen/AnnouncementDetailScreen.kt
@@ -40,6 +40,8 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.combo.runcombi.analytics.AnalyticsEvent
+import com.combo.runcombi.analytics.logButtonClick
import com.combo.runcombi.core.designsystem.component.NetworkImage
import com.combo.runcombi.core.designsystem.component.RunCombiAppTopBar
import com.combo.runcombi.core.designsystem.component.RunCombiButton
@@ -100,7 +102,9 @@ fun AnnouncementDetailScreen(
.fillMaxSize()
.background(Grey01)
) {
- val onApplyClick: (String) -> Unit = { url -> viewModel.openEventApplyUrl(url) }
+ val onApplyClick: (String) -> Unit = { url ->
+ viewModel.analyticsHelper.logButtonClick("onApplyClick", "AnnouncementDetailScreen")
+ viewModel.openEventApplyUrl(url) }
Column(modifier = Modifier.fillMaxSize()) {
Box(
@@ -191,7 +195,9 @@ fun AnnouncementDetailContent(
TitleSection(
title = detail.title,
startDate = detail.startDate,
- endDate = detail.endDate
+ endDate = detail.endDate,
+ announcementType = detail.announcementType,
+ regDate = detail.regDate
)
Spacer(modifier = Modifier.height(16.dp))
@@ -222,6 +228,8 @@ fun TitleSection(
title: String,
startDate: String,
endDate: String,
+ announcementType: String,
+ regDate: String,
) {
Column {
Text(
@@ -232,11 +240,19 @@ fun TitleSection(
Spacer(modifier = Modifier.height(8.dp))
- Text(
- text = "기간: ${FormatUtils.formatDate(startDate)} ~ ${FormatUtils.formatDate(endDate)}",
- style = body3,
- color = Grey06
- )
+ if (announcementType == "EVENT") {
+ Text(
+ text = "기간: ${FormatUtils.formatDate(startDate)} ~ ${FormatUtils.formatDate(endDate)}",
+ style = body3,
+ color = Grey06
+ )
+ } else {
+ Text(
+ text = "등록일: ${FormatUtils.formatDate(regDate)}",
+ style = body3,
+ color = Grey06
+ )
+ }
}
}
diff --git a/feature/setting/src/main/java/com/combo/runcombi/setting/viewmodel/AnnouncementDetailViewModel.kt b/feature/setting/src/main/java/com/combo/runcombi/setting/viewmodel/AnnouncementDetailViewModel.kt
index 5ca0830..425ea08 100644
--- a/feature/setting/src/main/java/com/combo/runcombi/setting/viewmodel/AnnouncementDetailViewModel.kt
+++ b/feature/setting/src/main/java/com/combo/runcombi/setting/viewmodel/AnnouncementDetailViewModel.kt
@@ -2,6 +2,7 @@ package com.combo.runcombi.setting.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import com.combo.runcombi.analytics.AnalyticsHelper
import com.combo.runcombi.common.DomainResult
import com.combo.runcombi.setting.model.AnnouncementDetailEvent
import com.combo.runcombi.setting.model.AnnouncementDetailUiState
@@ -22,6 +23,7 @@ import javax.inject.Inject
@HiltViewModel
class AnnouncementDetailViewModel @Inject constructor(
private val getAnnouncementDetailUseCase: GetAnnouncementDetailUseCase,
+ val analyticsHelper: AnalyticsHelper
) : ViewModel() {
private val _uiState = MutableStateFlow(AnnouncementDetailUiState())
val uiState: StateFlow = _uiState.asStateFlow()
diff --git a/feature/signup/src/main/java/com/combo/runcombi/signup/viewmodel/CompleteViewModel.kt b/feature/signup/src/main/java/com/combo/runcombi/signup/viewmodel/CompleteViewModel.kt
new file mode 100644
index 0000000..23c68a5
--- /dev/null
+++ b/feature/signup/src/main/java/com/combo/runcombi/signup/viewmodel/CompleteViewModel.kt
@@ -0,0 +1,12 @@
+package com.combo.runcombi.signup.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.combo.runcombi.analytics.AnalyticsHelper
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+
+@HiltViewModel
+class CompleteViewModel @Inject constructor(
+ val analyticsHelper: AnalyticsHelper,
+) : ViewModel()
diff --git a/feature/walk/src/main/java/com/combo/runcombi/walk/screen/WalkMainScreen.kt b/feature/walk/src/main/java/com/combo/runcombi/walk/screen/WalkMainScreen.kt
index c0d05fd..7b2c710 100644
--- a/feature/walk/src/main/java/com/combo/runcombi/walk/screen/WalkMainScreen.kt
+++ b/feature/walk/src/main/java/com/combo/runcombi/walk/screen/WalkMainScreen.kt
@@ -38,6 +38,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.combo.runcombi.analytics.logScreenView
import com.combo.runcombi.core.designsystem.component.NetworkImage
import com.combo.runcombi.core.designsystem.component.RunCombiBottomSheet
import com.combo.runcombi.core.designsystem.component.StableImage
@@ -78,7 +79,6 @@ import com.google.maps.android.compose.MapUiSettings
import com.google.maps.android.compose.rememberCameraPositionState
import kotlinx.coroutines.flow.collectLatest
-
@OptIn(ExperimentalPermissionsApi::class)
@SuppressLint("MissingPermission")
@Composable
@@ -92,8 +92,11 @@ fun WalkMainScreen(
val uiState by walkMainViewModel.uiState.collectAsStateWithLifecycle()
val locationPermissionState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
var showPermissionSettingSheet by remember { mutableStateOf(false) }
+ val analyticsHelper = walkMainViewModel.analyticsHelper
LaunchedEffect(Unit) {
+ analyticsHelper.logScreenView("WalkMainScreen")
+
if (!locationPermissionState.status.isGranted) {
locationPermissionState.launchPermissionRequest()
}
diff --git a/feature/walk/src/main/java/com/combo/runcombi/walk/screen/WalkTrackingScreen.kt b/feature/walk/src/main/java/com/combo/runcombi/walk/screen/WalkTrackingScreen.kt
index f254070..eaccf71 100644
--- a/feature/walk/src/main/java/com/combo/runcombi/walk/screen/WalkTrackingScreen.kt
+++ b/feature/walk/src/main/java/com/combo/runcombi/walk/screen/WalkTrackingScreen.kt
@@ -1,4 +1,5 @@
-@file:OptIn(ExperimentalFoundationApi::class, ExperimentalFoundationApi::class,
+@file:OptIn(
+ ExperimentalFoundationApi::class, ExperimentalFoundationApi::class,
ExperimentalFoundationApi::class, ExperimentalFoundationApi::class
)
@@ -45,6 +46,8 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.combo.runcombi.analytics.logScreenView
+import com.combo.runcombi.analytics.logWalkCompleted
import com.combo.runcombi.core.designsystem.component.NetworkImage
import com.combo.runcombi.core.designsystem.component.RunCombiBottomSheet
import com.combo.runcombi.core.designsystem.component.StableImage
@@ -91,6 +94,7 @@ fun WalkTrackingScreen(
walkRecordViewModel: WalkTrackingViewModel = hiltViewModel(),
) {
val context = LocalContext.current
+ val analyticsHelper = walkRecordViewModel.analyticsHelper
val uiState by walkRecordViewModel.uiState.collectAsStateWithLifecycle()
val isPaused = uiState.isPaused
val fusedLocationClient = remember { LocationServices.getFusedLocationProviderClient(context) }
@@ -113,6 +117,8 @@ fun WalkTrackingScreen(
LaunchedEffect(isInitialized.value) {
if (!isInitialized.value) {
+ analyticsHelper.logScreenView("WalkTrackingScreen")
+
walkMainViewModel.startRun()
val member = walkMainViewModel.walkData.value.member
@@ -169,6 +175,11 @@ fun WalkTrackingScreen(
onAccept = {
when (showSheet.value) {
BottomSheetType.FINISH -> {
+ // 산책 완료 이벤트 로깅
+ val duration = FormatUtils.formatTime(uiState.time)
+ val distance = String.format("%.2f", uiState.distance / 1000.0)
+ analyticsHelper.logWalkCompleted(duration, "${distance}km")
+
walkMainViewModel.setResultData(
time = uiState.time,
distance = uiState.distance,
diff --git a/feature/walk/src/main/java/com/combo/runcombi/walk/viewmodel/WalkMainViewModel.kt b/feature/walk/src/main/java/com/combo/runcombi/walk/viewmodel/WalkMainViewModel.kt
index d4067c8..175c0b9 100644
--- a/feature/walk/src/main/java/com/combo/runcombi/walk/viewmodel/WalkMainViewModel.kt
+++ b/feature/walk/src/main/java/com/combo/runcombi/walk/viewmodel/WalkMainViewModel.kt
@@ -2,6 +2,7 @@ package com.combo.runcombi.walk.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import com.combo.runcombi.analytics.AnalyticsHelper
import com.combo.runcombi.common.DomainResult
import com.combo.runcombi.domain.user.model.Member
import com.combo.runcombi.domain.user.model.Pet
@@ -30,6 +31,7 @@ import com.combo.runcombi.walk.usecase.StartRunUseCase
class WalkMainViewModel @Inject constructor(
private val getUserInfoUseCase: GetUserInfoUseCase,
private val startRunUseCase: StartRunUseCase,
+ val analyticsHelper: AnalyticsHelper
) : ViewModel() {
private val _uiState = MutableStateFlow(WalkMainUiState())
val uiState: StateFlow = _uiState
diff --git a/feature/walk/src/main/java/com/combo/runcombi/walk/viewmodel/WalkTrackingViewModel.kt b/feature/walk/src/main/java/com/combo/runcombi/walk/viewmodel/WalkTrackingViewModel.kt
index 830fada..a7bfa48 100644
--- a/feature/walk/src/main/java/com/combo/runcombi/walk/viewmodel/WalkTrackingViewModel.kt
+++ b/feature/walk/src/main/java/com/combo/runcombi/walk/viewmodel/WalkTrackingViewModel.kt
@@ -2,6 +2,7 @@ package com.combo.runcombi.walk.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import com.combo.runcombi.analytics.AnalyticsHelper
import com.combo.runcombi.common.DomainResult
import com.combo.runcombi.walk.model.BottomSheetType
import com.combo.runcombi.walk.model.ExerciseType
@@ -34,6 +35,7 @@ class WalkTrackingViewModel @Inject constructor(
private val updateWalkRecordUseCase: UpdateWalkRecordUseCase,
private val calculatePetCalorieUseCase: CalculatePetCalorieUseCase,
private val calculateMemberCalorieUseCase: CalculateMemberCalorieUseCase,
+ val analyticsHelper: AnalyticsHelper,
) : ViewModel() {
private val _uiState = MutableStateFlow(WalkUiState())
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index c98ed89..ff0f000 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -51,6 +51,9 @@ lottie-compose = "6.5.0"
# Markdown
compose-markdown = "0.3.6"
+# Firebase
+firebase-bom = "32.7.4"
+
# Test
junit = "4.13.2"
junitVersion = "1.2.1"
@@ -58,6 +61,8 @@ espressoCore = "3.6.1"
mockk = "1.13.11"
v2User = "2.21.4"
playServicesLocation = "21.3.0"
+runtimeAndroid = "1.8.3"
+playServicesMeasurementApi = "23.0.0"
[libraries]
# AndroidX
@@ -137,6 +142,11 @@ v2-user = { module = "com.kakao.sdk:v2-user", version.ref = "v2User" }
maps-compose = { module = "com.google.maps.android:maps-compose", version.ref = "mapsCompose" }
play-services-location = { group = "com.google.android.gms", name = "play-services-location", version.ref = "playServicesLocation" }
+# Firebase
+firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebase-bom" }
+firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics-ktx" }
+firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics-ktx" }
+
[plugins]
# Android
android-application = { id = "com.android.application", version.ref = "agp" }
@@ -155,6 +165,10 @@ kotlin-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
# Hilt
hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
+# Firebase
+google-services = { id = "com.google.gms.google-services", version = "4.4.3" }
+firebase-crashlytics = { id = "com.google.firebase.crashlytics", version = "2.9.9" }
+
[bundles]
coroutines = ["coroutines-core", "coroutines-android"]
test = ["junit","coroutines-test","kotlin-test"]
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 75bcb44..48b70b1 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -50,3 +50,4 @@ include(":core:domain:history")
include(":core:data:history")
include(":core:domain:setting")
include(":core:data:setting")
+include(":core:analytics")