diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4ba127f1..d9fb9b64 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -67,6 +67,9 @@ + diff --git a/app/src/main/java/com/moive/app/MoiveApplication.kt b/app/src/main/java/com/moive/app/MoiveApplication.kt index 4eedb429..4b6fae45 100644 --- a/app/src/main/java/com/moive/app/MoiveApplication.kt +++ b/app/src/main/java/com/moive/app/MoiveApplication.kt @@ -3,6 +3,7 @@ package com.moive.app import android.app.Application import com.kakao.sdk.common.KakaoSdk import com.kakao.vectormap.KakaoMapSdk +import com.moive.app.core.fcm.NotificationChannels import dagger.hilt.android.HiltAndroidApp import timber.log.Timber @@ -12,6 +13,7 @@ class MoiveApplication : Application() { super.onCreate() initTimber() + NotificationChannels.createDefaultChannel(this) initKakaoSdk() initKakaoMapSdk() } diff --git a/app/src/main/java/com/moive/app/core/designsystem/component/toast/MoiveSnackbarHost.kt b/app/src/main/java/com/moive/app/core/designsystem/component/toast/MoiveSnackbarHost.kt index f43d2e5d..4e67c09c 100644 --- a/app/src/main/java/com/moive/app/core/designsystem/component/toast/MoiveSnackbarHost.kt +++ b/app/src/main/java/com/moive/app/core/designsystem/component/toast/MoiveSnackbarHost.kt @@ -1,6 +1,7 @@ package com.moive.app.core.designsystem.component.toast import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable @@ -22,7 +23,7 @@ fun MoiveSnackbarHost( text = visuals.message, type = visuals.type, modifier = Modifier - .padding(horizontal = 16.dp) + .padding(horizontal = 20.dp) .padding(bottom = 80.dp), ) } diff --git a/app/src/main/java/com/moive/app/core/fcm/MoiveFirebaseMessagingService.kt b/app/src/main/java/com/moive/app/core/fcm/MoiveFirebaseMessagingService.kt index e4aa09e5..a3825552 100644 --- a/app/src/main/java/com/moive/app/core/fcm/MoiveFirebaseMessagingService.kt +++ b/app/src/main/java/com/moive/app/core/fcm/MoiveFirebaseMessagingService.kt @@ -1,12 +1,9 @@ package com.moive.app.core.fcm import android.Manifest -import android.app.NotificationChannel -import android.app.NotificationManager import android.app.PendingIntent import android.content.Intent import android.content.pm.PackageManager -import android.os.Build import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat @@ -21,7 +18,6 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import timber.log.Timber -import java.util.concurrent.atomic.AtomicInteger import javax.inject.Inject @AndroidEntryPoint @@ -61,8 +57,8 @@ class MoiveFirebaseMessagingService: FirebaseMessagingService() { override fun onMessageReceived(remoteMessage: RemoteMessage) { super.onMessageReceived(remoteMessage) - val title = remoteMessage.data[MESSAGE_TITLE] ?: return - val body = remoteMessage.data[MESSAGE_BODY] + val title = remoteMessage.notification?.title ?: remoteMessage.data[MESSAGE_TITLE] ?: return + val body = remoteMessage.notification?.body ?: remoteMessage.data[MESSAGE_BODY] val meetingId = remoteMessage.data[MESSAGE_MEETING_ID]?.toLongOrNull() val notificationId = remoteMessage.data[MESSAGE_NOTIFICATION_ID]?.toLongOrNull() @@ -70,16 +66,7 @@ class MoiveFirebaseMessagingService: FirebaseMessagingService() { } private fun showNotification(title: String, body: String?, meetingId: Long?, notificationId: Long?) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val channel = NotificationChannel( - CHANNEL_ID, - CHANNEL_NAME, - NotificationManager.IMPORTANCE_HIGH, - ).apply { - enableVibration(true) - } - getSystemService(NotificationManager::class.java).createNotificationChannel(channel) - } + NotificationChannels.createDefaultChannel(this) val granted = ContextCompat.checkSelfPermission( this, @@ -87,7 +74,7 @@ class MoiveFirebaseMessagingService: FirebaseMessagingService() { ) == PackageManager.PERMISSION_GRANTED if (!granted) return - val androidNotificationId = notificationIdGenerator.incrementAndGet() + val androidNotificationId = (notificationId ?: System.currentTimeMillis()).toInt() val contentIntent = packageManager.getLaunchIntentForPackage(packageName)?.apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP @@ -101,7 +88,7 @@ class MoiveFirebaseMessagingService: FirebaseMessagingService() { PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, ) - val notification = NotificationCompat.Builder(this, CHANNEL_ID) + val notification = NotificationCompat.Builder(this, NotificationChannels.defaultChannelId(this)) .setSmallIcon(R.drawable.img_logo_white_bg) .setContentTitle(title) .setContentText(body) @@ -118,9 +105,6 @@ class MoiveFirebaseMessagingService: FirebaseMessagingService() { const val MESSAGE_NOTIFICATION_ID = "notificationId" private const val MESSAGE_TITLE = "title" private const val MESSAGE_BODY = "body" - private const val CHANNEL_ID = "moive_default_channel" - private const val CHANNEL_NAME = "일반 알림" private const val DEVICE_TOKEN_PUT_SUCCESS_MESSAGE = "디바이스 토큰 등록 성공" - private val notificationIdGenerator = AtomicInteger(0) } } diff --git a/app/src/main/java/com/moive/app/core/fcm/NotificationChannels.kt b/app/src/main/java/com/moive/app/core/fcm/NotificationChannels.kt new file mode 100644 index 00000000..a4176e69 --- /dev/null +++ b/app/src/main/java/com/moive/app/core/fcm/NotificationChannels.kt @@ -0,0 +1,27 @@ +package com.moive.app.core.fcm + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.os.Build +import com.moive.app.R + +object NotificationChannels { + private const val DEFAULT_CHANNEL_NAME = "일반 알림" + + fun defaultChannelId(context: Context): String = + context.getString(R.string.default_notification_channel_id) + + fun createDefaultChannel(context: Context) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return + + val channel = NotificationChannel( + defaultChannelId(context), + DEFAULT_CHANNEL_NAME, + NotificationManager.IMPORTANCE_HIGH, + ).apply { + enableVibration(true) + } + context.getSystemService(NotificationManager::class.java).createNotificationChannel(channel) + } +} diff --git a/app/src/main/java/com/moive/app/data/meeting/mapper/MeetingDetailMapper.kt b/app/src/main/java/com/moive/app/data/meeting/mapper/MeetingDetailMapper.kt index 3bc1e7fa..79c1dec6 100644 --- a/app/src/main/java/com/moive/app/data/meeting/mapper/MeetingDetailMapper.kt +++ b/app/src/main/java/com/moive/app/data/meeting/mapper/MeetingDetailMapper.kt @@ -32,4 +32,7 @@ fun ParticipantResponse.toModel(): ParticipantItemModel = statusLabel = participantStateLabel, isMe = isMe, isDone = participantState.endsWith("_DONE"), + isVoteDone = participantState == VOTE_DONE_STATE, ) + +private const val VOTE_DONE_STATE = "VOTE_DONE" diff --git a/app/src/main/java/com/moive/app/data/meeting/model/MeetingDetailModel.kt b/app/src/main/java/com/moive/app/data/meeting/model/MeetingDetailModel.kt index 696692f0..07e1159f 100644 --- a/app/src/main/java/com/moive/app/data/meeting/model/MeetingDetailModel.kt +++ b/app/src/main/java/com/moive/app/data/meeting/model/MeetingDetailModel.kt @@ -31,4 +31,5 @@ data class ParticipantItemModel( val statusLabel: String, val isMe: Boolean, val isDone: Boolean, + val isVoteDone: Boolean, ) diff --git a/app/src/main/java/com/moive/app/presentation/common/component/ShadowButton.kt b/app/src/main/java/com/moive/app/presentation/common/component/ShadowButton.kt index edba5535..8d33b554 100644 --- a/app/src/main/java/com/moive/app/presentation/common/component/ShadowButton.kt +++ b/app/src/main/java/com/moive/app/presentation/common/component/ShadowButton.kt @@ -4,15 +4,14 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.unit.dp import com.moive.app.core.designsystem.component.button.MoiveButton import com.moive.app.core.designsystem.component.button.MoiveButtonSize import com.moive.app.core.designsystem.component.button.MoiveButtonType import com.moive.app.core.designsystem.theme.MoiveTheme.colors -import com.moive.app.core.designsystem.theme.MoiveTheme.radius import com.moive.app.core.extensions.customShadow @Composable @@ -30,10 +29,7 @@ fun ShadowButton( .then( if (showShadow) { Modifier.customShadow( - shape = RoundedCornerShape( - topStart = radius.md, - topEnd = radius.md, - ), + shape = RectangleShape, color = colors.shadowBlack8, blur = 12.dp, ) @@ -43,10 +39,6 @@ fun ShadowButton( ) .background( color = colors.background.default00, - shape = RoundedCornerShape( - topStart = radius.md, - topEnd = radius.md, - ), ) ) { MoiveButton( diff --git a/app/src/main/java/com/moive/app/presentation/condition/ConditionViewModel.kt b/app/src/main/java/com/moive/app/presentation/condition/ConditionViewModel.kt index 3aae3bf4..98a2ee76 100644 --- a/app/src/main/java/com/moive/app/presentation/condition/ConditionViewModel.kt +++ b/app/src/main/java/com/moive/app/presentation/condition/ConditionViewModel.kt @@ -133,7 +133,7 @@ class ConditionViewModel @Inject constructor( } } - fun onDateNextClick() { + fun onDateSaveClick() { val state = _uiState.value val pending = state.pendingDateTime val isNewDate = pending != null && state.confirmedDateTimes.none { @@ -156,7 +156,7 @@ class ConditionViewModel @Inject constructor( } } - fun onDateSaveClick() { + fun onDateNextClick() { _uiState.update { it.copy( isDateBottomSheetVisible = false, diff --git a/app/src/main/java/com/moive/app/presentation/condition/component/DateBottomSheet.kt b/app/src/main/java/com/moive/app/presentation/condition/component/DateBottomSheet.kt index 8ca85f46..0a7e6381 100644 --- a/app/src/main/java/com/moive/app/presentation/condition/component/DateBottomSheet.kt +++ b/app/src/main/java/com/moive/app/presentation/condition/component/DateBottomSheet.kt @@ -90,6 +90,7 @@ fun DateBottomSheet( text = "저장", type = MoiveButtonType.TERTIARY, size = MoiveButtonSize.LARGE, + enabled = state.pendingDateTime != null, onClick = onSaveDateClick, modifier = Modifier.weight(1f), ) @@ -97,7 +98,6 @@ fun DateBottomSheet( text = "다음", type = MoiveButtonType.PRIMARY, size = MoiveButtonSize.LARGE, - enabled = state.pendingDateTime != null, onClick = onNextDateClick, modifier = Modifier.weight(1f), ) diff --git a/app/src/main/java/com/moive/app/presentation/home/HomeContract.kt b/app/src/main/java/com/moive/app/presentation/home/HomeContract.kt index 54d7b4af..cf716e57 100644 --- a/app/src/main/java/com/moive/app/presentation/home/HomeContract.kt +++ b/app/src/main/java/com/moive/app/presentation/home/HomeContract.kt @@ -2,6 +2,7 @@ package com.moive.app.presentation.home import androidx.compose.runtime.Immutable import com.moive.app.data.home.mapper.MeetingTab +import com.moive.app.data.home.mapper.toMeetingTab import com.moive.app.data.home.model.MyMeetingCardItemModel import com.moive.app.data.home.model.ConfirmedMeetingItemModel import kotlinx.collections.immutable.ImmutableList @@ -27,6 +28,13 @@ interface HomeContract { val displayedMyMeetingList: ImmutableList get() = myMeetingList.take(MY_MEETING_LIST_MAX_SIZE).toImmutableList() + val emptyMyMeetingMessage: String + get() = when (selectedTab.toMeetingTab()) { + MeetingTab.ALL -> "아직 참여 중인 모임이 없어요." + MeetingTab.UPCOMING -> "예정된 모임이 없어요." + MeetingTab.PAST -> "지난 모임이 없어요." + } + companion object { private const val MY_MEETING_LIST_MAX_SIZE = 5 } diff --git a/app/src/main/java/com/moive/app/presentation/home/HomeScreen.kt b/app/src/main/java/com/moive/app/presentation/home/HomeScreen.kt index 8bde06d2..3139c36d 100644 --- a/app/src/main/java/com/moive/app/presentation/home/HomeScreen.kt +++ b/app/src/main/java/com/moive/app/presentation/home/HomeScreen.kt @@ -223,7 +223,7 @@ private fun HomeScreen( ) Text( - text = "아직 참여 중인 모임이 없어요.", + text = uiState.emptyMyMeetingMessage, color = colors.text.subtle, style = typography.body.smNormalR, ) diff --git a/app/src/main/java/com/moive/app/presentation/home/component/ConfirmedMeetingCardItem.kt b/app/src/main/java/com/moive/app/presentation/home/component/ConfirmedMeetingCardItem.kt index 36ed6262..1a33fa7a 100644 --- a/app/src/main/java/com/moive/app/presentation/home/component/ConfirmedMeetingCardItem.kt +++ b/app/src/main/java/com/moive/app/presentation/home/component/ConfirmedMeetingCardItem.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.moive.app.R @@ -101,6 +102,8 @@ fun ConfirmedMeetingCardItem( text = location, color = colors.text.onBg, style = typography.body.smNormalR, + maxLines = 1, + overflow = TextOverflow.Ellipsis, ) } diff --git a/app/src/main/java/com/moive/app/presentation/login/component/AgreementBottomSheet.kt b/app/src/main/java/com/moive/app/presentation/login/component/AgreementBottomSheet.kt index 717c89b6..a5818214 100644 --- a/app/src/main/java/com/moive/app/presentation/login/component/AgreementBottomSheet.kt +++ b/app/src/main/java/com/moive/app/presentation/login/component/AgreementBottomSheet.kt @@ -17,8 +17,10 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -36,6 +38,7 @@ import com.moive.app.core.designsystem.component.bottomsheet.MoiveBottomSheet import com.moive.app.core.designsystem.component.button.MoiveButton import com.moive.app.core.designsystem.component.button.MoiveButtonSize import com.moive.app.core.designsystem.component.button.MoiveButtonType +import com.moive.app.core.designsystem.component.toast.LocalSnackbarHostState import com.moive.app.core.designsystem.theme.MoiveTheme import com.moive.app.core.designsystem.theme.MoiveTheme.colors import com.moive.app.core.designsystem.theme.MoiveTheme.radius @@ -137,7 +140,7 @@ private fun AllAgreementButton( shape = RoundedCornerShape(radius.md), ) .background( - color = if (isPressed) colors.fill.default06 else colors.fill.default07, + color = if (isPressed || isChecked) colors.fill.default06 else colors.fill.default07, shape = RoundedCornerShape(radius.md), ) .noRippleClickable(onClick = { onAllAgreementClick(!isChecked) }) @@ -216,31 +219,34 @@ private fun AgreementBottomSheetPreview() { var isServiceAgreed by remember { mutableStateOf(false) } var isPrivacyAgreed by remember { mutableStateOf(false) } var isMarketingAgreed by remember { mutableStateOf(false) } + val snackbarHostState = remember { SnackbarHostState() } + + CompositionLocalProvider(LocalSnackbarHostState provides snackbarHostState) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + MoiveButton( + text = "약관동의 바텀시트 열기", + type = MoiveButtonType.PRIMARY, + size = MoiveButtonSize.LARGE, + onClick = { showBottomSheet = true }, + ) + } - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - MoiveButton( - text = "약관동의 바텀시트 열기", - type = MoiveButtonType.PRIMARY, - size = MoiveButtonSize.LARGE, - onClick = { showBottomSheet = true }, - ) - } - - if (showBottomSheet) { - AgreementBottomSheet( - isServiceAgreed = isServiceAgreed, - isPrivacyAgreed = isPrivacyAgreed, - isMarketingAgreed = isMarketingAgreed, - isConfirmEnabled = isServiceAgreed && isPrivacyAgreed, - onServiceClick = { isServiceAgreed = it }, - onPrivacyClick = { isPrivacyAgreed = it }, - onMarketingClick = { isMarketingAgreed = it }, - onConfirmClick = {}, - onDismissRequest = { showBottomSheet = false }, - ) + if (showBottomSheet) { + AgreementBottomSheet( + isServiceAgreed = isServiceAgreed, + isPrivacyAgreed = isPrivacyAgreed, + isMarketingAgreed = isMarketingAgreed, + isConfirmEnabled = isServiceAgreed && isPrivacyAgreed, + onServiceClick = { isServiceAgreed = it }, + onPrivacyClick = { isPrivacyAgreed = it }, + onMarketingClick = { isMarketingAgreed = it }, + onConfirmClick = {}, + onDismissRequest = { showBottomSheet = false }, + ) + } } } } diff --git a/app/src/main/java/com/moive/app/presentation/main/MainActivity.kt b/app/src/main/java/com/moive/app/presentation/main/MainActivity.kt index 484075bf..39907aa9 100644 --- a/app/src/main/java/com/moive/app/presentation/main/MainActivity.kt +++ b/app/src/main/java/com/moive/app/presentation/main/MainActivity.kt @@ -155,14 +155,15 @@ class MainActivity : ComponentActivity() { private fun Intent.extractInviteCode(): String? = takeIf { it.action == Intent.ACTION_VIEW }?.data?.lastPathSegment -private fun Intent.extractMeetingId(): Long? { - val meetingId = getLongExtra(MoiveFirebaseMessagingService.MESSAGE_MEETING_ID, -1L) - if (meetingId != -1L) removeExtra(MoiveFirebaseMessagingService.MESSAGE_MEETING_ID) - return meetingId.takeIf { it != -1L } -} +private fun Intent.extractMeetingId(): Long? = + extractLongExtra(MoiveFirebaseMessagingService.MESSAGE_MEETING_ID) + +private fun Intent.extractNotificationId(): Long? = + extractLongExtra(MoiveFirebaseMessagingService.MESSAGE_NOTIFICATION_ID) -private fun Intent.extractNotificationId(): Long? { - val notificationId = getLongExtra(MoiveFirebaseMessagingService.MESSAGE_NOTIFICATION_ID, -1L) - if (notificationId != -1L) removeExtra(MoiveFirebaseMessagingService.MESSAGE_NOTIFICATION_ID) - return notificationId.takeIf { it != -1L } +private fun Intent.extractLongExtra(key: String): Long? { + val value = getLongExtra(key, -1L).takeIf { it != -1L } + ?: getStringExtra(key)?.toLongOrNull() + removeExtra(key) + return value } diff --git a/app/src/main/java/com/moive/app/presentation/main/component/MainBottomBar.kt b/app/src/main/java/com/moive/app/presentation/main/component/MainBottomBar.kt index c9981e30..93b2755d 100644 --- a/app/src/main/java/com/moive/app/presentation/main/component/MainBottomBar.kt +++ b/app/src/main/java/com/moive/app/presentation/main/component/MainBottomBar.kt @@ -122,8 +122,8 @@ private fun MeetingFloatingButton( .customShadow( shape = RoundedCornerShape(radius.circular), color = colors.shadow20, - blur = 10.dp, - offsetY = 4.dp, + blur = 5.dp, + offsetY = 2.dp, ) .background( color = colors.fill.default00, diff --git a/app/src/main/java/com/moive/app/presentation/meeting/create/component/MeetingCreationContent.kt b/app/src/main/java/com/moive/app/presentation/meeting/create/component/MeetingCreationContent.kt index 558bb239..aaaddd57 100644 --- a/app/src/main/java/com/moive/app/presentation/meeting/create/component/MeetingCreationContent.kt +++ b/app/src/main/java/com/moive/app/presentation/meeting/create/component/MeetingCreationContent.kt @@ -137,6 +137,7 @@ fun MeetingCreationContent( onKeyboardAction = { focusManager.clearFocus() }, + isError = uiState.isMeetingScheduleFormatInvalid, ) if (uiState.isMeetingScheduleFormatInvalid) { diff --git a/app/src/main/java/com/moive/app/presentation/meeting/detail/MeetingDetailScreen.kt b/app/src/main/java/com/moive/app/presentation/meeting/detail/MeetingDetailScreen.kt index 4b738069..26d8ed39 100644 --- a/app/src/main/java/com/moive/app/presentation/meeting/detail/MeetingDetailScreen.kt +++ b/app/src/main/java/com/moive/app/presentation/meeting/detail/MeetingDetailScreen.kt @@ -2,13 +2,13 @@ package com.moive.app.presentation.meeting.detail import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -19,15 +19,18 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.zIndex import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LocalLifecycleOwner @@ -97,6 +100,7 @@ fun MeetingDetailRoute( uiState.scheduledDate, uiState.scheduledTime, ) + else -> Unit } }, @@ -137,6 +141,11 @@ private fun MeetingDetailScreen( val isContentScrollable by remember { derivedStateOf { lazyListState.canScrollForward || lazyListState.canScrollBackward } } + val density = LocalDensity.current + var toolTipHeight by remember { mutableStateOf(0.dp) } + val isToolTipVisible = uiState.toolTipMessage.isNotEmpty() + val toolTipReservedHeight = + if (isToolTipVisible) toolTipHeight + 4.dp else 0.dp Column( modifier = modifier @@ -150,74 +159,83 @@ private fun MeetingDetailScreen( onTrailingIconClick = onMoreClick, ) - LazyColumn( - state = lazyListState, + Box( modifier = Modifier.weight(1f), - contentPadding = PaddingValues(horizontal = 20.dp, vertical = 20.dp), ) { - item { - MeetingInfoRow( - thumbnailRes = uiState.thumbnailType.toMeetingThumbnailRes(), - meetingName = uiState.meetingName, - meetingPurpose = uiState.meetingPurpose, - ) + LazyColumn( + state = lazyListState, + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + start = 20.dp, + top = 20.dp, + end = 20.dp, + bottom = 20.dp + toolTipReservedHeight, + ), + ) { + item { + MeetingInfoRow( + thumbnailRes = uiState.thumbnailType.toMeetingThumbnailRes(), + meetingName = uiState.meetingName, + meetingPurpose = uiState.meetingPurpose, + ) - Spacer(modifier = Modifier.height(20.dp)) + Spacer(modifier = Modifier.height(20.dp)) - MoiveButton( - text = "친구 초대", - icon = ImageVector.vectorResource(R.drawable.ic_mail_fill_20), - type = MoiveButtonType.TERTIARY, - size = MoiveButtonSize.MEDIUM, - onClick = onInviteFriendClick, - ) + MoiveButton( + text = "친구 초대", + icon = ImageVector.vectorResource(R.drawable.ic_mail_fill_20), + type = MoiveButtonType.TERTIARY, + size = MoiveButtonSize.MEDIUM, + onClick = onInviteFriendClick, + ) - Spacer(modifier = Modifier.height(32.dp)) + Spacer(modifier = Modifier.height(32.dp)) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), - ) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_user_16), - contentDescription = null, - tint = colors.icon.tertiary, - ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_user_16), + contentDescription = null, + tint = colors.icon.tertiary, + ) - Text( - text = "참여자 ${uiState.participants.size}명", - color = colors.text.tertiary, - style = typography.label.xsR, - ) + Text( + text = "참여자 ${uiState.participants.size}명", + color = colors.text.tertiary, + style = typography.label.xsR, + ) + } + + Spacer(modifier = Modifier.height(12.dp)) } - Spacer(modifier = Modifier.height(12.dp)) + items( + items = uiState.participants, + key = { it.id } + ) { participant -> + ParticipantItem( + participant = participant, + status = uiState.status, + onActionButtonClick = onActionButtonClick, + ) + + Spacer(modifier = Modifier.height(12.dp)) + } } - items( - items = uiState.participants, - key = { it.id } - ) { participant -> - ParticipantItem( - participant = participant, - status = uiState.status, - onActionButtonClick = onActionButtonClick, + if (isToolTipVisible) { + MoiveToolTip( + text = uiState.toolTipMessage, + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = 4.dp) + .onSizeChanged { toolTipHeight = with(density) { it.height.toDp() } }, ) - - Spacer(modifier = Modifier.height(12.dp)) } } - if (uiState.toolTipMessage.isNotEmpty()) { - MoiveToolTip( - text = uiState.toolTipMessage, - modifier = Modifier - .align(Alignment.CenterHorizontally) - .zIndex(1f) - .offset(y = 8.dp), - ) - } - ShadowButton( text = uiState.primaryActionLabel, isEnabled = uiState.primaryActionEnabled, @@ -241,7 +259,7 @@ private fun MeetingDetailScreen( } } -@Preview(showBackground = true,) +@Preview(showBackground = true) @Composable private fun MeetingDetailScreenPreview() { MoiveTheme { @@ -260,6 +278,7 @@ private fun MeetingDetailScreenPreview() { statusLabel = "조건 입력 전", isMe = true, isDone = false, + isVoteDone = false, ), ParticipantItemModel( id = 2L, @@ -268,6 +287,7 @@ private fun MeetingDetailScreenPreview() { statusLabel = "조건 입력 완료", isMe = false, isDone = true, + isVoteDone = false, ), ), ), diff --git a/app/src/main/java/com/moive/app/presentation/meeting/detail/MeetingDetailViewModel.kt b/app/src/main/java/com/moive/app/presentation/meeting/detail/MeetingDetailViewModel.kt index c357f73f..fa66440c 100644 --- a/app/src/main/java/com/moive/app/presentation/meeting/detail/MeetingDetailViewModel.kt +++ b/app/src/main/java/com/moive/app/presentation/meeting/detail/MeetingDetailViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute +import com.moive.app.data.meeting.mapper.MeetingStatus import com.moive.app.data.meeting.repository.MeetingRepository import com.moive.app.data.meeting.repository.NotMeetingParticipantException import com.moive.app.presentation.meeting.detail.MeetingDetailContract.SideEffect @@ -43,6 +44,7 @@ class MeetingDetailViewModel @Inject constructor( meetingRepository.getMeetingDetail(meetingId) .onSuccess { detail -> + val isMyVoteDone = detail.participants.any { it.isMe && it.isVoteDone } _uiState.update { it.copy( meetingDetailUiState = MeetingDetailUiState.Success, @@ -58,7 +60,8 @@ class MeetingDetailViewModel @Inject constructor( participants = detail.participants, toolTipMessage = detail.homeMessage ?: "", primaryActionLabel = detail.primaryActionLabel ?: "", - primaryActionEnabled = detail.primaryActionEnabled, + primaryActionEnabled = detail.primaryActionEnabled && + !(detail.status == MeetingStatus.VOTING && isMyVoteDone), ) } } diff --git a/app/src/main/java/com/moive/app/presentation/meeting/detail/component/ParticipantItem.kt b/app/src/main/java/com/moive/app/presentation/meeting/detail/component/ParticipantItem.kt index 04e13f2c..4be407d3 100644 --- a/app/src/main/java/com/moive/app/presentation/meeting/detail/component/ParticipantItem.kt +++ b/app/src/main/java/com/moive/app/presentation/meeting/detail/component/ParticipantItem.kt @@ -1,5 +1,6 @@ package com.moive.app.presentation.meeting.detail.component +import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -13,6 +14,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp import com.moive.app.core.designsystem.component.button.MoiveButton import com.moive.app.core.designsystem.component.button.MoiveButtonSize @@ -40,6 +42,10 @@ fun ParticipantItem( color = if (participant.isMe) colors.stroke.default03 else colors.stroke.default04, shape = RoundedCornerShape(radius.md), ) + .background( + color = if (participant.isMe) colors.fill.default08 else colors.fill.default07, + shape = RoundedCornerShape(radius.md), + ) .padding(12.dp), verticalArrangement = Arrangement.spacedBy(16.dp), ) { @@ -52,6 +58,7 @@ fun ParticipantItem( modifier = Modifier .size(40.dp) .clip(RoundedCornerShape(radius.circular)), + contentScale = ContentScale.Crop, ) Column( diff --git a/app/src/main/java/com/moive/app/presentation/meeting/list/MeetingListContract.kt b/app/src/main/java/com/moive/app/presentation/meeting/list/MeetingListContract.kt index 232ffc29..600a1b06 100644 --- a/app/src/main/java/com/moive/app/presentation/meeting/list/MeetingListContract.kt +++ b/app/src/main/java/com/moive/app/presentation/meeting/list/MeetingListContract.kt @@ -2,6 +2,7 @@ package com.moive.app.presentation.meeting.list import androidx.compose.runtime.Immutable import com.moive.app.data.home.mapper.MeetingTab +import com.moive.app.data.home.mapper.toMeetingTab import com.moive.app.data.meeting.model.MeetingListCardItemModel import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -20,6 +21,13 @@ interface MeetingListContract { MeetingTab.UPCOMING.label, MeetingTab.PAST.label, ) + + val emptyMeetingMessage: String + get() = when (selectedTab.toMeetingTab()) { + MeetingTab.ALL -> "아직 참여 중인 모임이 없어요." + MeetingTab.UPCOMING -> "예정된 모임이 없어요." + MeetingTab.PAST -> "지난 모임이 없어요." + } } } diff --git a/app/src/main/java/com/moive/app/presentation/meeting/list/MeetingListScreen.kt b/app/src/main/java/com/moive/app/presentation/meeting/list/MeetingListScreen.kt index 1a02a51c..8a222b83 100644 --- a/app/src/main/java/com/moive/app/presentation/meeting/list/MeetingListScreen.kt +++ b/app/src/main/java/com/moive/app/presentation/meeting/list/MeetingListScreen.kt @@ -118,7 +118,7 @@ private fun MeetingListScreen( Spacer(modifier = Modifier.height(16.dp)) Text( - text = "아직 참여 중인 모임이 없어요.", + text = uiState.emptyMeetingMessage, color = colors.text.subtle, style = typography.body.smNormalR, ) diff --git a/app/src/main/java/com/moive/app/presentation/splash/SplashContract.kt b/app/src/main/java/com/moive/app/presentation/splash/SplashContract.kt index cd58ad7b..c7e1394a 100644 --- a/app/src/main/java/com/moive/app/presentation/splash/SplashContract.kt +++ b/app/src/main/java/com/moive/app/presentation/splash/SplashContract.kt @@ -1,6 +1,7 @@ package com.moive.app.presentation.splash import androidx.compose.runtime.Immutable +import com.moive.app.core.designsystem.component.toast.ToastType interface SplashContract { @@ -11,6 +12,7 @@ interface SplashContract { sealed interface SideEffect { data class NavigateToHome(val isAutoLoginSuccess: Boolean) : SideEffect + data class OnShowToast(val msg: String, val type: ToastType) : SideEffect } } diff --git a/app/src/main/java/com/moive/app/presentation/splash/SplashOverlay.kt b/app/src/main/java/com/moive/app/presentation/splash/SplashOverlay.kt index c2defe74..65b3cdb8 100644 --- a/app/src/main/java/com/moive/app/presentation/splash/SplashOverlay.kt +++ b/app/src/main/java/com/moive/app/presentation/splash/SplashOverlay.kt @@ -22,9 +22,11 @@ import com.airbnb.lottie.compose.LottieCompositionSpec import com.airbnb.lottie.compose.animateLottieCompositionAsState import com.airbnb.lottie.compose.rememberLottieComposition import com.moive.app.R +import com.moive.app.core.designsystem.component.toast.LocalToastTrigger import com.moive.app.core.designsystem.theme.MoiveTheme import com.moive.app.core.designsystem.theme.MoiveTheme.colors import com.moive.app.presentation.splash.SplashContract.SideEffect.NavigateToHome +import com.moive.app.presentation.splash.SplashContract.SideEffect.OnShowToast @Composable fun SplashOverlay( @@ -34,6 +36,7 @@ fun SplashOverlay( viewModel: SplashViewModel = hiltViewModel(), ) { val lifeCycleOwner = LocalLifecycleOwner.current + val showToast = LocalToastTrigger.current val uiState by viewModel.uiState.collectAsStateWithLifecycle() var isAnimationEnded by remember { mutableStateOf(false) } @@ -56,6 +59,8 @@ fun SplashOverlay( if (sideEffect.isAutoLoginSuccess) navigateToHome() else navigateToLogin() viewModel.onReady() } + + is OnShowToast -> showToast(sideEffect.msg, sideEffect.type) } } } diff --git a/app/src/main/java/com/moive/app/presentation/splash/SplashViewModel.kt b/app/src/main/java/com/moive/app/presentation/splash/SplashViewModel.kt index b687fb7e..f4c9fa82 100644 --- a/app/src/main/java/com/moive/app/presentation/splash/SplashViewModel.kt +++ b/app/src/main/java/com/moive/app/presentation/splash/SplashViewModel.kt @@ -2,9 +2,11 @@ package com.moive.app.presentation.splash import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.moive.app.core.designsystem.component.toast.ToastType import com.moive.app.core.utils.suspendRunCatching import com.moive.app.data.auth.repository.AuthRepository import com.moive.app.presentation.splash.SplashContract.SideEffect.NavigateToHome +import com.moive.app.presentation.splash.SplashContract.SideEffect.OnShowToast import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.async import kotlinx.coroutines.channels.Channel @@ -15,6 +17,7 @@ import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import timber.log.Timber +import java.io.IOException import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds @@ -44,16 +47,18 @@ class SplashViewModel @Inject constructor( } delayTime.await() - val isAutoLoginSuccess = reissueToken.await() + val reissueResult = reissueToken.await() .onSuccess { Timber.tag(AUTHORIZATION).d(AUTO_LOGIN_SUCCESS_MESSAGE) } .onFailure { error -> Timber.tag(AUTHORIZATION).e("$AUTO_LOGIN_FAILURE_MESSAGE $error") } - .isSuccess - _sideEffect.send(NavigateToHome(isAutoLoginSuccess = isAutoLoginSuccess)) + if (reissueResult.exceptionOrNull() is IOException) { + _sideEffect.send(OnShowToast(NETWORK_ERROR_MESSAGE, ToastType.ERROR)) + } + _sideEffect.send(NavigateToHome(isAutoLoginSuccess = reissueResult.isSuccess)) } } @@ -76,6 +81,7 @@ class SplashViewModel @Inject constructor( private const val REISSUE_FAILURE_MESSAGE = "토큰 재발급 실패" private const val AUTO_LOGIN_SUCCESS_MESSAGE = "자동 로그인 성공" private const val AUTO_LOGIN_FAILURE_MESSAGE = "자동 로그인 실패" + private const val NETWORK_ERROR_MESSAGE = "네트워크 연결을 확인해주세요." } } diff --git a/app/src/main/res/drawable/img_pin_marker_1.png b/app/src/main/res/drawable/img_pin_marker_1.png index 66fd1cdc..c0844657 100644 Binary files a/app/src/main/res/drawable/img_pin_marker_1.png and b/app/src/main/res/drawable/img_pin_marker_1.png differ diff --git a/app/src/main/res/drawable/img_pin_marker_2.png b/app/src/main/res/drawable/img_pin_marker_2.png index dae6d2cd..3d4fc79c 100644 Binary files a/app/src/main/res/drawable/img_pin_marker_2.png and b/app/src/main/res/drawable/img_pin_marker_2.png differ diff --git a/app/src/main/res/drawable/img_pin_marker_3.png b/app/src/main/res/drawable/img_pin_marker_3.png index b7a7498e..bb0084f2 100644 Binary files a/app/src/main/res/drawable/img_pin_marker_3.png and b/app/src/main/res/drawable/img_pin_marker_3.png differ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 69304061..0d696b71 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,4 @@ MOIVE + moive_default_channel