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
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.eatssu.android.presentation.map

import com.eatssu.android.domain.model.Partnership
import com.eatssu.common.enums.PeriodType
import java.time.LocalDate

private val FESTIVAL_START_DATE: LocalDate = LocalDate.of(2026, 9, 15)
private val FESTIVAL_END_DATE: LocalDate = LocalDate.of(2026, 9, 16)

internal fun isFestivalPartnershipPeriod(date: LocalDate): Boolean =
!date.isBefore(FESTIVAL_START_DATE) && !date.isAfter(FESTIVAL_END_DATE)

internal fun activeFestivalPartnerships(
partnerships: List<Partnership>,
date: LocalDate,
): List<Partnership> {
if (!isFestivalPartnershipPeriod(date)) return emptyList()

return partnerships.mapNotNull { partnership ->
val activeFestivalInfos = partnership.partnershipInfos.filter { info ->
info.periodType == PeriodType.FESTIVAL && info.isActiveOn(date)
}

partnership
.takeIf { activeFestivalInfos.isNotEmpty() }
?.copy(partnershipInfos = activeFestivalInfos)
}
}

internal fun mergePartnerships(
userPartnerships: List<Partnership>,
festivalPartnerships: List<Partnership>,
): List<Partnership> {
val mergedByStoreName = linkedMapOf<String, Partnership>()

(userPartnerships + festivalPartnerships).forEach { partnership ->
val previous = mergedByStoreName[partnership.storeName]
mergedByStoreName[partnership.storeName] = if (previous == null) {
partnership
} else {
previous.copy(
partnershipInfos = (previous.partnershipInfos + partnership.partnershipInfos)
.distinctBy(Partnership.PartnershipInfo::id),
naverMapUrl = previous.naverMapUrl ?: partnership.naverMapUrl,
kakaoMapUrl = previous.kakaoMapUrl ?: partnership.kakaoMapUrl,
)
}
}

return mergedByStoreName.values.toList()
}

internal val Partnership.hasFestivalPartnership: Boolean
get() = partnershipInfos.any { it.periodType == PeriodType.FESTIVAL }

private fun Partnership.PartnershipInfo.isActiveOn(date: LocalDate): Boolean {
val start = startDate.toLocalDateOrNull() ?: return false
val end = endDate.toLocalDateOrNull() ?: return false
return !date.isBefore(start) && !date.isAfter(end)
}

private fun String.toLocalDateOrNull(): LocalDate? = runCatching(LocalDate::parse).getOrNull()
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import android.content.Intent
import android.content.pm.PackageManager
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement.spacedBy
Expand Down Expand Up @@ -41,6 +40,7 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
Expand All @@ -50,7 +50,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
Expand All @@ -70,9 +69,11 @@ import com.eatssu.android.presentation.MainState
import com.eatssu.android.presentation.MainViewModel
import com.eatssu.android.presentation.goodprice.GoodPriceMapRoute
import com.eatssu.android.presentation.map.component.DepartmentBottomSheet
import com.eatssu.android.presentation.map.component.FestivalPartnershipHelp
import com.eatssu.android.presentation.map.component.MapRestaurantBottomSheet
import com.eatssu.android.presentation.map.component.PartnershipCategory
import com.eatssu.android.presentation.map.component.PartnershipCategoryFilterRow
import com.eatssu.android.presentation.map.component.PartnershipMarkerIcon
import com.eatssu.android.presentation.mypage.userinfo.UserInfoActivity
import com.eatssu.android.presentation.util.TrackScreenViewEvent
import com.eatssu.android.presentation.util.showToast
Expand All @@ -81,7 +82,6 @@ import com.eatssu.common.UiState
import com.eatssu.common.UiText
import com.eatssu.common.analytics.MapAnalyticsEvent
import com.eatssu.common.enums.ScreenId
import com.eatssu.common.enums.StoreType
import com.eatssu.common.enums.ToastType
import com.eatssu.design_system.theme.EatssuTheme
import com.eatssu.design_system.theme.Gray300
Expand Down Expand Up @@ -423,6 +423,11 @@ internal fun MapScreen(
selectedCategory: PartnershipCategory,
) {
val analyticsTracker = LocalAnalyticsTracker.current
var showFestivalHelp by rememberSaveable { mutableStateOf(false) }

LaunchedEffect(mapState.hasFestivalPartnerships) {
if (!mapState.hasFestivalPartnerships) showFestivalHelp = false
}

// 학과 정보가 없을 때 보여줄 BottomSheet
if (departmentSheetState.isVisible) {
Expand Down Expand Up @@ -530,17 +535,7 @@ internal fun MapScreen(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = spacedBy(3.dp),
) {
val iconRes = when (partnership.restaurantType) {
StoreType.CAFE -> R.drawable.ic_map_marker_cafe
StoreType.PUB -> R.drawable.ic_map_marker_pub
else -> R.drawable.ic_map_marker_restaurant
}

Image(
painter = painterResource(id = iconRes),
contentDescription = null,
modifier = Modifier.size(20.dp),
)
PartnershipMarkerIcon(partnership = partnership)

Text(
text = partnership.storeName,
Expand Down Expand Up @@ -580,6 +575,19 @@ internal fun MapScreen(
},
modifier = Modifier.padding(top = 12.dp),
)

if (mapState.hasFestivalPartnerships) {
FestivalPartnershipHelp(
isMessageVisible = showFestivalHelp,
onClick = { showFestivalHelp = !showFestivalHelp },
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(
end = 24.dp,
bottom = dimensionResource(R.dimen.bottom_nav_height) + 12.dp,
),
)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import java.time.Clock
import java.time.LocalDate
import javax.inject.Inject


Expand All @@ -42,6 +46,9 @@ data class MapState(
val selectedCategory: PartnershipCategory = PartnershipCategory.ALL,
val filterChangeResult: FilterChangeResult? = null,
) {
val hasFestivalPartnerships: Boolean
get() = partnerships.any { it.hasFestivalPartnership }

val visiblePartnerships: List<Partnership>
get() = selectedCategory.storeType?.let { storeType ->
partnerships.filter { it.restaurantType == storeType }
Expand All @@ -59,6 +66,7 @@ class MapViewModel @Inject constructor(
private val getPartnershipDetailUseCase: GetPartnershipDetailUseCase,
private val getUserCollegeDepartmentUseCase: GetUserCollegeDepartmentUseCase,
private val analyticsTracker: AnalyticsTracker,
private val clock: Clock = Clock.systemDefaultZone(),
) : ViewModel() {

private val _uiState: MutableStateFlow<UiState<MapState>> = MutableStateFlow(UiState.Init)
Expand Down Expand Up @@ -153,7 +161,27 @@ class MapViewModel @Inject constructor(

_uiState.value = UiState.Loading

val partnerships = partnershipRepository.getUserCollegePartnerships()
val today = LocalDate.now(clock)
val partnerships = coroutineScope {
val userPartnerships = async {
partnershipRepository.getUserCollegePartnerships()
}
val festivalPartnerships = async {
if (isFestivalPartnershipPeriod(today)) {
activeFestivalPartnerships(
partnerships = partnershipRepository.getAllPartnerships(),
date = today,
)
} else {
emptyList()
}
}

mergePartnerships(
userPartnerships = userPartnerships.await(),
festivalPartnerships = festivalPartnerships.await(),
)
}
_uiState.value = UiState.Success(
currentData.copy(
partnerships = partnerships,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package com.eatssu.android.presentation.map.component

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement.spacedBy
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.eatssu.android.R
import com.eatssu.design_system.theme.EatssuTheme
import com.eatssu.design_system.theme.White
import com.eatssu.design_system.theme.pretendardMedium

private val FestivalWine = Color(0xFF880A19)

@Composable
internal fun FestivalPartnershipHelp(
isMessageVisible: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier,
horizontalArrangement = spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
if (isMessageVisible) {
Box(modifier = Modifier.width(223.dp)) {
Image(
painter = painterResource(R.drawable.bg_festival_partnership_help),
contentDescription = null,
modifier = Modifier.size(width = 222.dp, height = 48.dp),
)

Column(
modifier = Modifier
.padding(start = 13.dp, top = 6.dp),
) {
FestivalHelpText(text = stringResource(R.string.festival_partnership_help_title))
FestivalHelpText(text = stringResource(R.string.festival_partnership_help_warning))
}
}
}

Box(
modifier = Modifier
.size(28.dp)
.background(FestivalWine.copy(alpha = 0.9f), CircleShape)
.clickable(onClick = onClick),
contentAlignment = Alignment.Center,
) {
Image(
painter = painterResource(R.drawable.ic_festival_partnership_info),
contentDescription = stringResource(R.string.festival_partnership_help_content_description),
modifier = Modifier.size(14.533.dp),
)
}
}
}

@Composable
private fun FestivalHelpText(text: String) {
Text(
text = text,
color = White,
maxLines = 1,
style = EatssuTheme.typography.caption2.copy(
fontFamily = pretendardMedium,
),
)
}

@Preview(showBackground = true)
@Composable
private fun FestivalPartnershipHelpPreview() {
EatssuTheme {
FestivalPartnershipHelp(
isMessageVisible = true,
onClick = {},
modifier = Modifier.padding(16.dp),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.eatssu.android.presentation.map.component

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.eatssu.android.R
import com.eatssu.android.domain.model.Partnership
import com.eatssu.android.presentation.map.hasFestivalPartnership
import com.eatssu.android.presentation.map.iconRes
import com.eatssu.common.enums.StoreType

private val FestivalWine = Color(0xFF880A19)

@Composable
internal fun PartnershipMarkerIcon(
partnership: Partnership,
modifier: Modifier = Modifier,
) {
if (partnership.hasFestivalPartnership) {
Box(
modifier = modifier
.size(20.dp)
.background(FestivalWine, CircleShape),
contentAlignment = Alignment.Center,
) {
Image(
painter = painterResource(id = partnership.restaurantType.iconRes),
contentDescription = null,
modifier = Modifier.size(14.dp),
)
}
} else {
val iconRes = when (partnership.restaurantType) {
StoreType.CAFE -> R.drawable.ic_map_marker_cafe
StoreType.PUB -> R.drawable.ic_map_marker_pub
else -> R.drawable.ic_map_marker_restaurant
}

Image(
painter = painterResource(id = iconRes),
contentDescription = null,
modifier = modifier.size(20.dp),
)
}
}
10 changes: 10 additions & 0 deletions app/src/main/res/drawable/bg_festival_partnership_help.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="222dp"
android:height="48dp"
android:viewportWidth="222"
android:viewportHeight="48">
<path
android:fillAlpha="0.9"
android:fillColor="#880A19"
android:pathData="M192,0C203.998,0 213.94,8.805 215.717,20.306L221.5,23.645C222.167,24.029 222.167,24.992 221.5,25.377L215.513,28.833C213.276,39.771 203.599,48 192,48H24C10.745,48 0,37.255 0,24C0,10.745 10.745,0 24,0H192Z" />
</vector>
Loading
Loading