diff --git a/app/src/main/java/com/eatssu/android/data/remote/dto/response/PartnershipResponse.kt b/app/src/main/java/com/eatssu/android/data/remote/dto/response/PartnershipResponse.kt index f4107fc30..3d4cd0e80 100644 --- a/app/src/main/java/com/eatssu/android/data/remote/dto/response/PartnershipResponse.kt +++ b/app/src/main/java/com/eatssu/android/data/remote/dto/response/PartnershipResponse.kt @@ -1,6 +1,7 @@ package com.eatssu.android.data.remote.dto.response import com.eatssu.android.domain.model.Partnership +import com.eatssu.common.enums.PeriodType import com.eatssu.common.enums.StoreType import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -37,7 +38,9 @@ data class PartnershipResponse( @SerialName("startDate") val startDate: String? = null, @SerialName("endDate") - val endDate: String? = null + val endDate: String? = null, + @SerialName("periodType") + val periodType: PeriodType = PeriodType.NORMAL ) } @@ -57,7 +60,8 @@ fun PartnershipResponse.toDomain(): Partnership = isLiked = it.isLiked ?: false, description = it.description ?: "", startDate = it.startDate ?: "", - endDate = it.endDate ?: "" + endDate = it.endDate ?: "", + periodType = it.periodType ) } ) diff --git a/app/src/main/java/com/eatssu/android/domain/model/Partnership.kt b/app/src/main/java/com/eatssu/android/domain/model/Partnership.kt index 3d2cfd825..50c8833b1 100644 --- a/app/src/main/java/com/eatssu/android/domain/model/Partnership.kt +++ b/app/src/main/java/com/eatssu/android/domain/model/Partnership.kt @@ -1,5 +1,6 @@ package com.eatssu.android.domain.model +import com.eatssu.common.enums.PeriodType import com.eatssu.common.enums.StoreType data class Partnership( @@ -18,6 +19,7 @@ data class Partnership( val isLiked: Boolean, val description: String, val startDate: String, - val endDate: String + val endDate: String, + val periodType: PeriodType, ) } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/presentation/map/MapViewModel.kt b/app/src/main/java/com/eatssu/android/presentation/map/MapViewModel.kt index 1c93a58a3..cc091d484 100644 --- a/app/src/main/java/com/eatssu/android/presentation/map/MapViewModel.kt +++ b/app/src/main/java/com/eatssu/android/presentation/map/MapViewModel.kt @@ -14,6 +14,7 @@ import com.eatssu.common.UiEvent import com.eatssu.common.UiState import com.eatssu.common.analytics.AnalyticsTracker import com.eatssu.common.analytics.MapAnalyticsEvent +import com.eatssu.common.enums.PeriodType import com.eatssu.common.enums.StoreType import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow @@ -76,25 +77,28 @@ class MapViewModel @Inject constructor( val userCollegeDepartment = getUserCollegeDepartmentUseCase() val newDepartmentId = userCollegeDepartment.userDepartment.departmentId.toLong() val newCollegeId = userCollegeDepartment.userCollege.collegeId.toLong() + // Festival 존재 여부를 판단하고, All/Festival 초기 목록에도 재사용한다. + val allPartnerships = partnershipRepository.getAllPartnerships() _departmentId.value = newDepartmentId _collegeId.value = newCollegeId - // departmentId가 변경되면 필터 자동 설정 - val current = uiState.value - val currentData = if (current is UiState.Success) current.data else MapState() - val initialFilter = if (newDepartmentId == -1L) FilterType.All else FilterType.Mine - + // Festival 제휴가 하나라도 있으면 Festival을 우선하고, 없으면 기존 기본 필터 규칙을 따른다. + val initialFilter = when { + allPartnerships.hasFestivalPartnership() -> FilterType.Festival + newDepartmentId == -1L -> FilterType.All + else -> FilterType.Mine + } _uiState.value = UiState.Success( - MapState(selectedFilter = initialFilter) + MapState(selectedFilter = initialFilter), ) - // 초기 필터에 따라 데이터 로드 when (initialFilter) { - FilterType.All -> loadPartnerships() + FilterType.All -> loadPartnerships(prefetchedPartnerships = allPartnerships) + FilterType.Festival -> loadFestivalPartnerships(prefetchedPartnerships = allPartnerships) FilterType.Mine -> loadUserCollegePartnerships() } - + Timber.d("학과 정보 : ${userCollegeDepartment.userDepartment.departmentName}") } } @@ -130,6 +134,10 @@ class MapViewModel @Inject constructor( analyticsTracker.track(MapAnalyticsEvent.AllClicked) } + FilterType.Festival -> { + loadFestivalPartnerships() + } + FilterType.Mine -> { loadUserCollegePartnerships() analyticsTracker.track( @@ -143,19 +151,51 @@ class MapViewModel @Inject constructor( } // 제휴 정보 로딩 - private fun loadPartnerships() { + private fun loadPartnerships( + prefetchedPartnerships: List? = null, + ) { viewModelScope.launch { val current = uiState.value val currentData = if (current is UiState.Success) current.data else MapState() - - _uiState.value = UiState.Loading - val partnerships = partnershipRepository.getAllPartnerships() + if (prefetchedPartnerships == null) + _uiState.value = UiState.Loading + + val partnerships = prefetchedPartnerships ?: partnershipRepository.getAllPartnerships() _uiState.value = UiState.Success( currentData.copy( partnerships = partnerships, - filterChangeResult = null + filterChangeResult = null, + ), + ) + } + } + + private fun loadFestivalPartnerships( + prefetchedPartnerships: List? = null, + ) { + viewModelScope.launch { + val current = uiState.value + val currentData = if (current is UiState.Success) current.data else MapState() + + if (prefetchedPartnerships == null) + _uiState.value = UiState.Loading + + val partnerships = partnershipRepository.getAllPartnerships().mapNotNull { + val festivalInfos = + it.partnershipInfos.filter { info -> info.periodType == PeriodType.FESTIVAL } + if (festivalInfos.isEmpty()) return@mapNotNull null + + it.copy( + partnershipInfos = festivalInfos ) + } + + _uiState.value = UiState.Success( + currentData.copy( + partnerships = partnerships, + filterChangeResult = null, + ), ) } } @@ -172,8 +212,8 @@ class MapViewModel @Inject constructor( _uiState.value = UiState.Success( currentData.copy( partnerships = partnerships, - filterChangeResult = null - ) + filterChangeResult = null, + ), ) } } @@ -205,8 +245,22 @@ class MapViewModel @Inject constructor( data.copy( restaurantPartnershipInfo = representative, restaurantInfoList = restaurantInfoList, - storeType = representative.storeType - ) + storeType = representative.storeType, + ), ) } } + +private fun List.hasFestivalPartnership(): Boolean = + any { partnership -> + partnership.partnershipInfos.any { info -> info.periodType == PeriodType.FESTIVAL } + } + +private fun List.festivalPartnerships(): List = + mapNotNull { partnership -> + val festivalInfos = + partnership.partnershipInfos.filter { info -> info.periodType == PeriodType.FESTIVAL } + if (festivalInfos.isEmpty()) return@mapNotNull null + + partnership.copy(partnershipInfos = festivalInfos) + } diff --git a/app/src/main/java/com/eatssu/android/presentation/map/component/PartnershipToggleItem.kt b/app/src/main/java/com/eatssu/android/presentation/map/component/PartnershipToggleItem.kt index cdb4bc484..6246b9a68 100644 --- a/app/src/main/java/com/eatssu/android/presentation/map/component/PartnershipToggleItem.kt +++ b/app/src/main/java/com/eatssu/android/presentation/map/component/PartnershipToggleItem.kt @@ -27,8 +27,9 @@ import com.eatssu.design_system.theme.White import timber.log.Timber enum class FilterType(@StringRes val labelResId: Int) { + Festival(R.string.partnership_filter_festival), + All(R.string.partnership_filter_all), Mine(R.string.partnership_filter_mine), - All(R.string.partnership_filter_all) } @Composable @@ -42,6 +43,7 @@ fun FilterType.getLabel(departmentName: String): String { departmentName } } + FilterType.Festival -> stringResource(labelResId) FilterType.All -> stringResource(labelResId) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d1560e119..738c82833 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -296,6 +296,7 @@ 영업 시간 찜한 제휴 내 제휴 + 축제 전체 학과 diff --git a/app/src/test/java/com/eatssu/android/domain/usecase/user/GetPartnershipDetailUseCaseBehaviorSpec.kt b/app/src/test/java/com/eatssu/android/domain/usecase/user/GetPartnershipDetailUseCaseBehaviorSpec.kt index 54dbc104b..2691ca839 100644 --- a/app/src/test/java/com/eatssu/android/domain/usecase/user/GetPartnershipDetailUseCaseBehaviorSpec.kt +++ b/app/src/test/java/com/eatssu/android/domain/usecase/user/GetPartnershipDetailUseCaseBehaviorSpec.kt @@ -2,6 +2,7 @@ package com.eatssu.android.domain.usecase.user import com.eatssu.android.domain.model.Partnership import com.eatssu.android.test.AppBehaviorSpec +import com.eatssu.common.enums.PeriodType import com.eatssu.common.enums.StoreType import io.kotest.matchers.shouldBe @@ -20,6 +21,7 @@ class GetPartnershipDetailUseCaseBehaviorSpec : AppBehaviorSpec({ description = "10% 할인", startDate = "2025-01-01", endDate = "2025-12-31", + periodType = PeriodType.NORMAL ), Partnership.PartnershipInfo( id = 2, @@ -31,6 +33,7 @@ class GetPartnershipDetailUseCaseBehaviorSpec : AppBehaviorSpec({ description = "음료 증정", startDate = "2025-02-01", endDate = "2025-11-30", + periodType = PeriodType.NORMAL ), ) val partnerships = listOf( diff --git a/app/src/test/java/com/eatssu/android/presentation/map/MapViewModelBehaviorSpec.kt b/app/src/test/java/com/eatssu/android/presentation/map/MapViewModelBehaviorSpec.kt index a1aacf867..ef922e9c1 100644 --- a/app/src/test/java/com/eatssu/android/presentation/map/MapViewModelBehaviorSpec.kt +++ b/app/src/test/java/com/eatssu/android/presentation/map/MapViewModelBehaviorSpec.kt @@ -11,20 +11,18 @@ import com.eatssu.android.test.AppBehaviorSpec import com.eatssu.android.test.samplePartnership import com.eatssu.android.test.samplePartnershipRestaurant import com.eatssu.android.test.sampleUserInfo +import com.eatssu.common.UiState import com.eatssu.common.analytics.AnalyticsTracker import com.eatssu.common.analytics.MapAnalyticsEvent -import com.eatssu.common.UiState +import com.eatssu.common.enums.PeriodType import com.eatssu.common.enums.StoreType import io.kotest.assertions.nondeterministic.eventually import io.kotest.matchers.shouldBe -import io.mockk.Runs import io.mockk.clearMocks import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every -import io.mockk.just import io.mockk.mockk -import io.mockk.mockkObject import io.mockk.verify import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay @@ -170,6 +168,7 @@ class MapViewModelBehaviorSpec : AppBehaviorSpec({ description = "10% 할인", startDate = "2025-01-01", endDate = "2025-12-31", + periodType = PeriodType.NORMAL, ), Partnership.PartnershipInfo( id = 2, @@ -181,6 +180,7 @@ class MapViewModelBehaviorSpec : AppBehaviorSpec({ description = "음료 증정", startDate = "2025-02-01", endDate = "2025-11-30", + periodType = PeriodType.NORMAL, ), ) val partnerships = listOf( @@ -310,6 +310,7 @@ class MapViewModelBehaviorSpec : AppBehaviorSpec({ description = "할인", startDate = "2025-01-01", endDate = "2025-12-31", + periodType = PeriodType.NORMAL, ) ), ) diff --git a/app/src/test/java/com/eatssu/android/test/TestFixtures.kt b/app/src/test/java/com/eatssu/android/test/TestFixtures.kt index ee01a4f5b..62aa2cef2 100644 --- a/app/src/test/java/com/eatssu/android/test/TestFixtures.kt +++ b/app/src/test/java/com/eatssu/android/test/TestFixtures.kt @@ -8,6 +8,7 @@ import com.eatssu.android.domain.model.Review import com.eatssu.android.domain.model.ReviewInfo import com.eatssu.android.domain.model.Token import com.eatssu.android.domain.model.UserInfo +import com.eatssu.common.enums.PeriodType import com.eatssu.common.enums.StoreType fun sampleCollege( @@ -83,6 +84,7 @@ fun samplePartnership( description = "desc", startDate = "2025-01-01", endDate = "2025-12-31", + periodType = PeriodType.NORMAL, ), ), type: StoreType = StoreType.CAFE, diff --git a/core/common/src/main/java/com/eatssu/common/enums/PeriodType.kt b/core/common/src/main/java/com/eatssu/common/enums/PeriodType.kt new file mode 100644 index 000000000..d51cd1ea6 --- /dev/null +++ b/core/common/src/main/java/com/eatssu/common/enums/PeriodType.kt @@ -0,0 +1,8 @@ +package com.eatssu.common.enums + +import kotlinx.serialization.Serializable + +@Serializable +enum class PeriodType { + FESTIVAL, NORMAL +} \ No newline at end of file