Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
792c044
feat/#99: ์ตœ์ƒ๋‹จ์—์„œ FilterChipRow ๋ฐฐ๊ฒฝ ํˆฌ๋ช… ์ฒ˜๋ฆฌ๋กœ ๋ฐฐ๋„ˆ ๊ทธ๋ฆผ์ž ๋…ธ์ถœ
DongChyeon Apr 28, 2026
4a9f671
revert/#99: ์ตœ์ƒ๋‹จ FilterChipRow ํˆฌ๋ช… ๋ฐฐ๊ฒฝ ์ฒ˜๋ฆฌ ๋กค๋ฐฑ
DongChyeon Apr 28, 2026
918dfe6
feat/#99: HomeFeedList๋ฅผ Box ์˜ค๋ฒ„๋ ˆ์ด ๋ ˆ์ด์•„์›ƒ์œผ๋กœ ์ „ํ™˜ํ•ด ๋ฐฐ๋„ˆ shadow ๋…ธ์ถœ
DongChyeon Apr 28, 2026
59c019c
Merge pull request #98 from Nexters/release/0.3.0
DongChyeon Apr 28, 2026
94ac557
Merge pull request #101 from Nexters/bugfix/#99-home-banner-shadow-clโ€ฆ
DongChyeon Apr 28, 2026
cd0bae8
style/#103: ํ™ˆ ํ™”๋ฉด EmptyView ์ƒ๋‹จ ๊ฐ„๊ฒฉ ์กฐ์ •
DongChyeon May 5, 2026
ff442e2
style/#103: ์—…๋กœ๋“œ ํ™”๋ฉด ์‚ฌ์ง„ stroke ์ถ”๊ฐ€
DongChyeon May 5, 2026
717ee33
style/#103: ActionSheet ๋””์ž์ธ ์ˆ˜์ •
DongChyeon May 5, 2026
62c4fb1
refactor/#103: ๋ฏธ์‚ฌ์šฉ ์ค‘์ธ `selectedFilter` ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ `HomeHeader` ์—์„œ ์ œ๊ฑฐ
DongChyeon May 5, 2026
e7a9410
style/#103: ํ™ˆ ํ™”๋ฉด ์นดํ…Œ๊ณ ๋ฆฌ ๋ฆฌ์ŠคํŠธ ํŒจ๋”ฉ ๋ฐ ๊ฐ„๊ฒฉ ์กฐ์ •
DongChyeon May 5, 2026
6404111
style/#103: NotificationScreen ํ•„ํ„ฐ ๊ฐ„๊ฒฉ ์ˆ˜์ • (8.dp -> 6.dp)
DongChyeon May 5, 2026
4872603
style/#103: ActionPopup ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€ ์—ฌ๋ฐฑ ์กฐ์ • (vertical padding ์ถ”๊ฐ€)
DongChyeon May 5, 2026
dc6c4a8
feat/#103: ํ™ˆ ํ™”๋ฉด ๋งํฌ ํˆดํŒ dismiss ์ƒํƒœ ๊ด€๋ฆฌ ๊ฐœ์„ 
DongChyeon May 5, 2026
ee12375
Merge pull request #104 from Nexters/feature/#103-design-qa
DongChyeon May 5, 2026
d42beaf
feat/#105: core/analytics ๋ชจ๋“ˆ ์ถ”๊ฐ€ (Mixpanel + DebugAnalytics)
DongChyeon May 7, 2026
70cc81a
feat/#105: feature/home Analytics ์—ฐ๋™ (feed_viewed, feed_exited, vote_โ€ฆ
DongChyeon May 7, 2026
1386a29
feat/#105: feature/upload Analytics ์—ฐ๋™ (vote_create_started, completeโ€ฆ
DongChyeon May 7, 2026
8546b0d
feat/#105: app ๋ชจ๋“ˆ์— core:analytics ์˜์กด์„ฑ ์ถ”๊ฐ€ (Hilt ๊ทธ๋ž˜ํ”„)
DongChyeon May 7, 2026
95acdce
refactor/#105: entrySource ์†์„ฑ ์ œ๊ฑฐ (๋‹จ์ผ ์ง„์ž… ๊ฒฝ๋กœ๋กœ ๋ถ„์„ ๊ฐ€์น˜ ์—†์Œ)
DongChyeon May 7, 2026
b68fc35
refactor/#105: FeedViewed์—์„œ entrySource ์ œ๊ฑฐ (๋‹จ์ผ ์ง„์ž… ๊ฒฝ๋กœ)
DongChyeon May 7, 2026
b81e213
chore/#105: GitHub Actions์— MIXPANEL_TOKEN secret ์ถ”๊ฐ€
DongChyeon May 7, 2026
3f1162d
fix/#105: VoteSubmitted ์ด๋ฒคํŠธ๋ฅผ API ์„ฑ๊ณต ์‹œ์ ์œผ๋กœ ์ด๋™
DongChyeon May 8, 2026
a299808
fix/#105: analytics.track ํ˜ธ์ถœ์„ ์ƒํƒœ ์—…๋ฐ์ดํŠธ/ํ™”๋ฉด ์ด๋™ ์ดํ›„๋กœ ์ด๋™
DongChyeon May 8, 2026
324fbe8
Merge pull request #106 from Nexters/feature/#105-feed-event-logging
DongChyeon May 8, 2026
bab7f20
fix/#107: FORCE ์ „๋žต ์‹œ latestVersion ์ด์ƒ ๋ฒ„์ „์€ ๊ฐ•์ œ ์—…๋ฐ์ดํŠธ ์ œ์™ธ
DongChyeon May 9, 2026
ac10c1b
fix/#107: ์‹œ๊ณ„ ์—ญํ–‰ ์ฒ˜๋ฆฌ, dismiss ๋กœ๊น…, ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ ์ถ”๊ฐ€
DongChyeon May 9, 2026
e5b1f1f
fix/#107: ์ฝ”๋“œ ๋ฆฌ๋ทฐ ๋ฐ˜์˜ - CancellationException ์žฌ์ „ํŒŒ, ์ฃผ์„ ์ถ”๊ฐ€, ์ตœ์ดˆ ์„ค์น˜ ํ…Œ์ŠคํŠธ
DongChyeon May 9, 2026
6e921ca
Merge pull request #108 from Nexters/feature/#107-fix-splash-update-lโ€ฆ
DongChyeon May 9, 2026
0dca3e7
chore: 0.3.0 (6) -> 0.3.1 (7)
DongChyeon May 9, 2026
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
3 changes: 3 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ trim_trailing_whitespace = true
[*.{kt,kts}]
ktlint_function_naming_ignore_when_annotated_with = Composable
ktlint_standard_annotation = disabled

[*Test.kt]
ktlint_standard_function-naming = disabled
1 change: 1 addition & 0 deletions .github/workflows/android-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ jobs:
echo "google.webClientId=${{ secrets.GOOGLE_WEB_CLIENT_ID }}" >> local.properties
echo "kakao.nativeAppKey=${{ secrets.KAKAO_NATIVE_APP_KEY }}" >> local.properties
echo "kakao.nativeAppKeyDebug=${{ secrets.KAKAO_NATIVE_APP_KEY_DEBUG }}" >> local.properties
echo "mixpanel.token=${{ secrets.MIXPANEL_TOKEN }}" >> local.properties

- name: Decode Keystore
run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > app/keystore.jks
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/distribute.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ jobs:
echo "google.webClientId=${{ secrets.GOOGLE_WEB_CLIENT_ID }}" >> local.properties
echo "kakao.nativeAppKey=${{ secrets.KAKAO_NATIVE_APP_KEY }}" >> local.properties
echo "kakao.nativeAppKeyDebug=${{ secrets.KAKAO_NATIVE_APP_KEY_DEBUG }}" >> local.properties
echo "mixpanel.token=${{ secrets.MIXPANEL_TOKEN }}" >> local.properties

- name: Decode Keystore
run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > app/keystore.jks
Expand Down
5 changes: 3 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ android {

defaultConfig {
applicationId = "com.sseotdabwa.buyornot"
versionCode = 6
versionName = "0.3.0"
versionCode = 7
versionName = "0.3.1"

buildConfigField("String", "KAKAO_NATIVE_APP_KEY", "\"${localProperties.getProperty("kakao.nativeAppKey", "")}\"")
manifestPlaceholders["NATIVE_APP_KEY"] = localProperties.getProperty("kakao.nativeAppKey", "")
Expand Down Expand Up @@ -101,6 +101,7 @@ android {

dependencies {
implementation(projects.domain)
implementation(projects.core.analytics)
implementation(projects.core.data)
implementation(projects.core.network)
implementation(projects.core.datastore)
Expand Down
43 changes: 43 additions & 0 deletions core/analytics/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import java.util.Properties

plugins {
id("buyornot.android.library")
alias(libs.plugins.hilt)
alias(libs.plugins.ksp)
}

val localProperties =
Properties().apply {
val localPropertiesFile = rootProject.file("local.properties")
if (localPropertiesFile.exists()) {
localPropertiesFile.inputStream().use { load(it) }
}
}

android {
namespace = "com.sseotdabwa.buyornot.core.analytics"

buildFeatures {
buildConfig = true
}

buildTypes {
debug {
buildConfigField("String", "MIXPANEL_TOKEN", "\"\"")
}
release {
buildConfigField(
"String",
"MIXPANEL_TOKEN",
"\"${localProperties.getProperty("mixpanel.token", "")}\"",
)
}
}
}

dependencies {
implementation(libs.mixpanel.android)

implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.sseotdabwa.buyornot.core.analytics

interface Analytics {
fun track(event: AnalyticsEvent)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.sseotdabwa.buyornot.core.analytics

sealed class AnalyticsEvent {
data class FeedViewed(
val firstVisibleItemIndex: Int,
) : AnalyticsEvent()

data class FeedExited(
val timeSpentSeconds: Float,
val lastVisibleItemIndex: Int,
) : AnalyticsEvent()

data class VoteSubmitted(
val feedId: Long,
val voteChoice: String,
val feedCategory: String,
) : AnalyticsEvent()

data class VoteCreateStarted(
val entrySource: String,
val isLoggedIn: Boolean,
) : AnalyticsEvent()

data class VoteCreateCompleted(
val itemId: Long,
val voteTitle: String,
val optionCount: Int,
) : AnalyticsEvent()

data class VoteCreateAbandoned(
val filledFields: List<String>,
val lastStep: String?,
) : AnalyticsEvent()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.sseotdabwa.buyornot.core.analytics

import android.util.Log

class DebugAnalytics : Analytics {
override fun track(event: AnalyticsEvent) {
Log.d("Analytics", event.toString())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.sseotdabwa.buyornot.core.analytics

import com.mixpanel.android.mpmetrics.MixpanelAPI
import org.json.JSONArray
import org.json.JSONObject

class MixpanelAnalytics(
private val mixpanel: MixpanelAPI,
) : Analytics {
override fun track(event: AnalyticsEvent) {
val (name, props) = event.toMixpanel()
mixpanel.track(name, props)
}

private fun AnalyticsEvent.toMixpanel(): Pair<String, JSONObject> {
val props = JSONObject()
val name =
when (this) {
is AnalyticsEvent.FeedViewed -> {
props.put("first_visible_item_index", firstVisibleItemIndex)
"feed_viewed"
}
is AnalyticsEvent.FeedExited -> {
props.put("time_spent_seconds", timeSpentSeconds)
props.put("last_visible_item_index", lastVisibleItemIndex)
"feed_exited"
}
is AnalyticsEvent.VoteSubmitted -> {
props.put("feed_id", feedId)
props.put("vote_choice", voteChoice)
props.put("feed_category", feedCategory)
"vote_submitted"
}
is AnalyticsEvent.VoteCreateStarted -> {
props.put("entry_source", entrySource)
props.put("is_logged_in", isLoggedIn)
"vote_create_started"
}
is AnalyticsEvent.VoteCreateCompleted -> {
props.put("item_id", itemId)
props.put("vote_title", voteTitle)
props.put("option_count", optionCount)
"vote_create_completed"
}
is AnalyticsEvent.VoteCreateAbandoned -> {
props.put("filled_fields", JSONArray(filledFields))
if (lastStep != null) props.put("last_step", lastStep)
"vote_create_abandoned"
}
}
return name to props
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.sseotdabwa.buyornot.core.analytics.di

import android.content.Context
import com.mixpanel.android.mpmetrics.MixpanelAPI
import com.sseotdabwa.buyornot.core.analytics.Analytics
import com.sseotdabwa.buyornot.core.analytics.BuildConfig
import com.sseotdabwa.buyornot.core.analytics.DebugAnalytics
import com.sseotdabwa.buyornot.core.analytics.MixpanelAnalytics
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object AnalyticsModule {
@Provides
@Singleton
fun provideAnalytics(
@ApplicationContext context: Context,
): Analytics =
if (BuildConfig.DEBUG) {
DebugAnalytics()
} else {
val mixpanel =
MixpanelAPI.getInstance(
context,
BuildConfig.MIXPANEL_TOKEN,
true,
)
MixpanelAnalytics(mixpanel)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,11 @@ fun ActionPopupContent(
) {
val pressedColor = BuyOrNotTheme.colors.gray200
Column(
modifier = Modifier.padding(6.dp),
modifier =
Modifier.padding(
horizontal = 6.dp,
vertical = 10.dp,
),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
items.forEach { (label, onClick) ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
Expand Down Expand Up @@ -64,7 +63,11 @@ fun ActionSheet(
LazyColumn(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(18.dp),
contentPadding = PaddingValues(vertical = 16.dp),
contentPadding =
PaddingValues(
top = 26.dp,
bottom = 30.dp,
),
) {
items(
count = actions.size,
Expand Down Expand Up @@ -92,9 +95,11 @@ private fun ActionItemRow(
modifier =
Modifier
.fillMaxWidth()
.height(30.dp)
.clickable(onClick = onClick)
.padding(horizontal = 24.dp),
.padding(
horizontal = 24.dp,
vertical = 6.dp,
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start,
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ fun FeedCard(
productLink: String? = null,
onLinkClick: (url: String) -> Unit = {},
showProductLinkTooltip: Boolean = false,
onTooltipDismiss: () -> Unit = {},
onImageClick: (imageUrls: List<String>, page: Int) -> Unit = { _, _ -> },
) {
val hasVoted = userVotedOptionIndex != null
Expand Down Expand Up @@ -140,7 +141,10 @@ fun FeedCard(
price = price,
productLink = productLink,
showTooltip = tooltipVisible,
onTooltipDismiss = { tooltipVisible = false },
onTooltipDismiss = {
tooltipVisible = false
onTooltipDismiss()
},
onFullscreenClick = { page -> onImageClick(productImageUrls, page) },
onLinkClick = onLinkClick,
)
Expand Down
4 changes: 4 additions & 0 deletions feature/auth/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,8 @@ dependencies {

implementation(platform(libs.firebase.bom))
implementation(libs.firebase.messaging)

testImplementation(libs.junit)
testImplementation(libs.kotlin.test)
testImplementation(libs.kotlinx.coroutines.test)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.util.Log
import androidx.core.content.pm.PackageInfoCompat
import androidx.lifecycle.viewModelScope
import com.sseotdabwa.buyornot.core.ui.base.BaseViewModel
import com.sseotdabwa.buyornot.domain.model.AppUpdateInfo
import com.sseotdabwa.buyornot.domain.model.UpdateStrategy
import com.sseotdabwa.buyornot.domain.model.UserType
import com.sseotdabwa.buyornot.domain.repository.AppPreferencesRepository
Expand All @@ -20,9 +21,36 @@ import javax.inject.Inject
import kotlin.coroutines.cancellation.CancellationException

private const val SPLASH_TIMEOUT_MILLIS = 2300L
private const val SOFT_UPDATE_INTERVAL_MILLIS = 24 * 60 * 60 * 1000L
internal const val SOFT_UPDATE_INTERVAL_MILLIS = 24 * 60 * 60 * 1000L
private const val TAG = "SplashUpdate"

internal fun resolveUpdateDialogType(
currentVersion: Int,
updateInfo: AppUpdateInfo?,
lastSoftUpdateShownTime: Long,
now: Long,
): UpdateDialogType {
if (updateInfo == null) return UpdateDialogType.None

return when {
currentVersion < updateInfo.minimumVersion -> UpdateDialogType.Force
// currentVersion >= latestVersion์ด๋ฉด ์ด๋ฏธ ์ตœ์‹  ๋ฒ„์ „์ด๋ฏ€๋กœ FORCE ํŒ์—… ํ‘œ์‹œ ์•ˆ ํ•จ
updateInfo.updateStrategy == UpdateStrategy.FORCE &&
currentVersion < updateInfo.latestVersion -> UpdateDialogType.Force
updateInfo.updateStrategy == UpdateStrategy.SOFT &&
currentVersion < updateInfo.latestVersion -> {
// lastSoftUpdateShownTime์ด ๋ฏธ๋ž˜ ๊ฐ’์ด๋ฉด ์‹œ๊ณ„ ์—ญํ–‰์œผ๋กœ ํŒ๋‹จ, ํ‘œ์‹œ๋œ ์  ์—†๋Š” ๊ฒƒ์œผ๋กœ ์ฒ˜๋ฆฌ
val effectiveLastShown = if (lastSoftUpdateShownTime > now) 0L else lastSoftUpdateShownTime
if (now - effectiveLastShown >= SOFT_UPDATE_INTERVAL_MILLIS) {
UpdateDialogType.Soft
} else {
UpdateDialogType.None
}
}
else -> UpdateDialogType.None
}
}

/**
* ์Šคํ”Œ๋ž˜์‹œ ํ™”๋ฉด์„ ์œ„ํ•œ ViewModel
*
Expand Down Expand Up @@ -93,30 +121,21 @@ class SplashViewModel @Inject constructor(

private suspend fun determineDialogType(
currentVersion: Int,
updateInfo: com.sseotdabwa.buyornot.domain.model.AppUpdateInfo?,
updateInfo: AppUpdateInfo?,
): UpdateDialogType {
if (updateInfo == null) return UpdateDialogType.None

return when {
currentVersion < updateInfo.minimumVersion -> UpdateDialogType.Force
updateInfo.updateStrategy == UpdateStrategy.FORCE -> UpdateDialogType.Force
updateInfo.updateStrategy == UpdateStrategy.SOFT &&
currentVersion < updateInfo.latestVersion -> {
val lastShown = appPreferencesRepository.lastSoftUpdateShownTime.first()
if (System.currentTimeMillis() - lastShown >= SOFT_UPDATE_INTERVAL_MILLIS) {
UpdateDialogType.Soft
} else {
UpdateDialogType.None
}
}
else -> UpdateDialogType.None
}
val now = System.currentTimeMillis()
val lastShown = appPreferencesRepository.lastSoftUpdateShownTime.first()
return resolveUpdateDialogType(currentVersion, updateInfo, lastShown, now)
}

private fun dismissSoftUpdate() {
viewModelScope.launch {
try {
appPreferencesRepository.updateLastSoftUpdateShownTime(System.currentTimeMillis())
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
Log.e(TAG, "Failed to save soft update shown time", e)
} finally {
updateState { it.copy(updateDialogType = UpdateDialogType.None) }
}
Expand Down
Loading
Loading