diff --git a/README.md b/README.md index c49a1f46..4dc78720 100644 --- a/README.md +++ b/README.md @@ -29,18 +29,18 @@ 떠내려온 편지를 통해 느린 소통의 감성을 경험해보세요. ## Features -- ✨ Feature 1 -- ✨ Feature 2 -- ✨ Feature 3 +- ✨ 매일 오후 6시마다 알림을 받고 떠내려오는 보틀에 호감을 표시해보세요. +- ✨ 호감을 받은 상대방이 수락하면 대화가 시작돼요. +- ✨ 상대와 끝까지 핑퐁(대화)이 완료되면 카카오톡 연락처를 얻어 서로를 이어줄게요. ## Screenshots
- - - - - + + + + +
## Architecture @@ -51,7 +51,7 @@

- +
## TechStack @@ -63,7 +63,8 @@ - **Networking**: Retrofit / OkHttp - **Database**: Proto-Datastore - **Async**: Coroutines -- **Others**: Coil / Lottie / Cloudy +- **Debugging Tool**: Firebase +- **Others**: Coil / Cloudy / Kakao-Sdk / FCM ## Contact & Contributor diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 564b794e..4923f88b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -75,6 +75,7 @@ dependencies { implementation(projects.feat.pingPong) implementation(projects.feat.splash) implementation(projects.feat.report) + implementation(projects.feat.setting) // Compose implementation(libs.androidx.compose.activity) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4e835ad9..ce018f09 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,6 +5,7 @@ + + android:resource="@drawable/bottle_notification_icon" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/bottle_notification_icon.xml b/app/src/main/res/drawable/bottle_notification_icon.xml new file mode 100644 index 00000000..d28a4e13 --- /dev/null +++ b/app/src/main/res/drawable/bottle_notification_icon.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/bottle_app_icon.xml b/app/src/main/res/mipmap-anydpi-v26/bottle_app_icon.xml index 0742a7d7..b89ea6a0 100644 --- a/app/src/main/res/mipmap-anydpi-v26/bottle_app_icon.xml +++ b/app/src/main/res/mipmap-anydpi-v26/bottle_app_icon.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/bottle_app_icon_round.xml b/app/src/main/res/mipmap-anydpi-v26/bottle_app_icon_round.xml index 0742a7d7..b89ea6a0 100644 --- a/app/src/main/res/mipmap-anydpi-v26/bottle_app_icon_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/bottle_app_icon_round.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/bottle_app_icon.webp b/app/src/main/res/mipmap-hdpi/bottle_app_icon.webp index cb966431..b70c586f 100644 Binary files a/app/src/main/res/mipmap-hdpi/bottle_app_icon.webp and b/app/src/main/res/mipmap-hdpi/bottle_app_icon.webp differ diff --git a/app/src/main/res/mipmap-hdpi/bottle_app_icon_round.webp b/app/src/main/res/mipmap-hdpi/bottle_app_icon_round.webp index 4ddadccd..6f3d3980 100644 Binary files a/app/src/main/res/mipmap-hdpi/bottle_app_icon_round.webp and b/app/src/main/res/mipmap-hdpi/bottle_app_icon_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/bottle_app_icon.webp b/app/src/main/res/mipmap-mdpi/bottle_app_icon.webp index dfffeb5d..896ee939 100644 Binary files a/app/src/main/res/mipmap-mdpi/bottle_app_icon.webp and b/app/src/main/res/mipmap-mdpi/bottle_app_icon.webp differ diff --git a/app/src/main/res/mipmap-mdpi/bottle_app_icon_round.webp b/app/src/main/res/mipmap-mdpi/bottle_app_icon_round.webp index 65661a96..481a909a 100644 Binary files a/app/src/main/res/mipmap-mdpi/bottle_app_icon_round.webp and b/app/src/main/res/mipmap-mdpi/bottle_app_icon_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/bottle_app_icon.webp b/app/src/main/res/mipmap-xhdpi/bottle_app_icon.webp index 50fc8ce1..8f55264f 100644 Binary files a/app/src/main/res/mipmap-xhdpi/bottle_app_icon.webp and b/app/src/main/res/mipmap-xhdpi/bottle_app_icon.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/bottle_app_icon_round.webp b/app/src/main/res/mipmap-xhdpi/bottle_app_icon_round.webp index 807aa443..8d878938 100644 Binary files a/app/src/main/res/mipmap-xhdpi/bottle_app_icon_round.webp and b/app/src/main/res/mipmap-xhdpi/bottle_app_icon_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/bottle_app_icon.webp b/app/src/main/res/mipmap-xxhdpi/bottle_app_icon.webp index 44d6df6a..ede4f70b 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/bottle_app_icon.webp and b/app/src/main/res/mipmap-xxhdpi/bottle_app_icon.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/bottle_app_icon_round.webp b/app/src/main/res/mipmap-xxhdpi/bottle_app_icon_round.webp index 3d4c436b..fbceaf60 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/bottle_app_icon_round.webp and b/app/src/main/res/mipmap-xxhdpi/bottle_app_icon_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/bottle_app_icon.webp b/app/src/main/res/mipmap-xxxhdpi/bottle_app_icon.webp index 06966e22..e1cb45ed 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/bottle_app_icon.webp and b/app/src/main/res/mipmap-xxxhdpi/bottle_app_icon.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/bottle_app_icon_round.webp b/app/src/main/res/mipmap-xxxhdpi/bottle_app_icon_round.webp index 0e89057b..7804c245 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/bottle_app_icon_round.webp and b/app/src/main/res/mipmap-xxxhdpi/bottle_app_icon_round.webp differ diff --git a/app/src/main/res/values/bottle_app_icon_background.xml b/app/src/main/res/values/bottle_app_icon_background.xml new file mode 100644 index 00000000..28337c0a --- /dev/null +++ b/app/src/main/res/values/bottle_app_icon_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/string.xml b/app/src/main/res/values/string.xml index a340b46a..b7b93f95 100644 --- a/app/src/main/res/values/string.xml +++ b/app/src/main/res/values/string.xml @@ -1,5 +1,5 @@ fcm_channel_id - fcm_channel_name + 푸쉬 알림 \ No newline at end of file diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index 050975d3..4de0bc26 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -11,6 +11,7 @@ dependencies { implementation(projects.core.domain) implementation(projects.core.network) implementation(projects.core.datastore) + implementation(projects.core.local) implementation(libs.jakewharton.timber) } \ No newline at end of file diff --git a/core/data/src/main/kotlin/com/team/bottles/core/data/mapper/AlimyResponseMapper.kt b/core/data/src/main/kotlin/com/team/bottles/core/data/mapper/AlimyResponseMapper.kt new file mode 100644 index 00000000..e1b332c0 --- /dev/null +++ b/core/data/src/main/kotlin/com/team/bottles/core/data/mapper/AlimyResponseMapper.kt @@ -0,0 +1,20 @@ +package com.team.bottles.core.data.mapper + +import com.team.bottles.core.domain.user.model.Notification +import com.team.bottles.core.domain.user.model.NotificationType +import com.team.bottles.network.dto.user.response.AlimyResponse +import com.team.bottles.network.dto.user.response.AlimyType + +fun AlimyResponse.toNotification(): Notification = + Notification( + notificationType = this.alimyType.toNotificationType(), + enabled = this.enabled + ) + +fun AlimyType.toNotificationType(): NotificationType = + when (this) { + AlimyType.MARKETING -> NotificationType.MARKETING + AlimyType.DAILY_RANDOM -> NotificationType.DAILY_RANDOM + AlimyType.PING_PONG -> NotificationType.PING_PONG + AlimyType.RECEIVE_LIKE -> NotificationType.RECEIVE_LIKE + } \ No newline at end of file diff --git a/core/data/src/main/kotlin/com/team/bottles/core/data/mapper/NotificationTypeMapper.kt b/core/data/src/main/kotlin/com/team/bottles/core/data/mapper/NotificationTypeMapper.kt new file mode 100644 index 00000000..2bf438ad --- /dev/null +++ b/core/data/src/main/kotlin/com/team/bottles/core/data/mapper/NotificationTypeMapper.kt @@ -0,0 +1,12 @@ +package com.team.bottles.core.data.mapper + +import com.team.bottles.core.domain.user.model.NotificationType +import com.team.bottles.network.dto.user.response.AlimyType + +fun NotificationType.toAlimyType(): AlimyType = + when (this) { + NotificationType.PING_PONG -> AlimyType.PING_PONG + NotificationType.MARKETING -> AlimyType.MARKETING + NotificationType.DAILY_RANDOM -> AlimyType.DAILY_RANDOM + NotificationType.RECEIVE_LIKE -> AlimyType.RECEIVE_LIKE + } \ No newline at end of file diff --git a/core/data/src/main/kotlin/com/team/bottles/core/data/mapper/UpdateAppVersionResponseMapper.kt b/core/data/src/main/kotlin/com/team/bottles/core/data/mapper/UpdateAppVersionResponseMapper.kt new file mode 100644 index 00000000..1e691fc4 --- /dev/null +++ b/core/data/src/main/kotlin/com/team/bottles/core/data/mapper/UpdateAppVersionResponseMapper.kt @@ -0,0 +1,9 @@ +package com.team.bottles.core.data.mapper + +import com.team.bottles.network.dto.auth.response.UpdateAppVersionResponse + +fun UpdateAppVersionResponse.toLatestVersionCode(): Int = + this.latestAndroidVersion?: 10007 + +fun UpdateAppVersionResponse.toMinimumVersionCode(): Int = + this.minimumAndroidVersion?: 10007 \ No newline at end of file diff --git a/core/data/src/main/kotlin/com/team/bottles/core/data/repository/AuthRepositoryImpl.kt b/core/data/src/main/kotlin/com/team/bottles/core/data/repository/AuthRepositoryImpl.kt index f29c6347..d4de122a 100644 --- a/core/data/src/main/kotlin/com/team/bottles/core/data/repository/AuthRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/team/bottles/core/data/repository/AuthRepositoryImpl.kt @@ -1,6 +1,8 @@ package com.team.bottles.core.data.repository import com.team.bottles.core.data.mapper.toAuthResult +import com.team.bottles.core.data.mapper.toLatestVersionCode +import com.team.bottles.core.data.mapper.toMinimumVersionCode import com.team.bottles.core.datastore.datasource.TokenDataSource import com.team.bottles.core.domain.auth.model.AuthResult import com.team.bottles.core.domain.auth.model.Token @@ -73,4 +75,10 @@ class AuthRepositoryImpl @Inject constructor( override suspend fun getSavedLocalFcmToken(): String = tokenDataSource.getFcmDeviceToken() + override suspend fun getLatestAppVersion(): Int = + authDataSource.fetchRequiredMinimumAppVersion().toLatestVersionCode() + + override suspend fun getRequiredAppVersion(): Int = + authDataSource.fetchRequiredMinimumAppVersion().toMinimumVersionCode() + } diff --git a/core/data/src/main/kotlin/com/team/bottles/core/data/repository/UserRepositoryImpl.kt b/core/data/src/main/kotlin/com/team/bottles/core/data/repository/UserRepositoryImpl.kt index d0c3b4b3..ee53d8ae 100644 --- a/core/data/src/main/kotlin/com/team/bottles/core/data/repository/UserRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/team/bottles/core/data/repository/UserRepositoryImpl.kt @@ -1,12 +1,19 @@ package com.team.bottles.core.data.repository +import com.team.bottles.core.data.mapper.toAlimyType +import com.team.bottles.core.data.mapper.toNotification +import com.team.bottles.core.domain.user.model.Notification import com.team.bottles.core.domain.user.repository.UserRepository +import com.team.bottles.local.datasource.DeviceDataSource import com.team.bottles.network.datasource.UserDataSource +import com.team.bottles.network.dto.auth.request.BlockContactListRequest +import com.team.bottles.network.dto.user.request.AlimyOnOffRequest import com.team.bottles.network.dto.user.request.ReportUserRequest import javax.inject.Inject class UserRepositoryImpl @Inject constructor( private val userDataSource: UserDataSource, + private val deviceDataSource: DeviceDataSource, ) : UserRepository { override suspend fun reportUser(userId: Int, contents: String) { @@ -18,4 +25,29 @@ class UserRepositoryImpl @Inject constructor( ) } + override suspend fun loadContacts(): List = + deviceDataSource.getContacts() + + override suspend fun updateBlockingContacts(contacts: List) { + userDataSource.updateWantToBlockContacts( + request = BlockContactListRequest( + blockContacts = contacts + ) + ) + } + + override suspend fun loadSettingNotifications(): List = + userDataSource.fetchSettingNotifications().map { response -> + response.toNotification() + } + + override suspend fun updateSettingNotification(notification: Notification) { + userDataSource.updateSettingNotification( + request = AlimyOnOffRequest( + alimyType = notification.notificationType.toAlimyType(), + enabled = notification.enabled + ) + ) + } + } \ No newline at end of file diff --git a/core/datastore/src/main/java/com/team/bottles/core/datastore/di/LocalDataSourceModule.kt b/core/datastore/src/main/java/com/team/bottles/core/datastore/di/DataStoreDataSourceModule.kt similarity index 91% rename from core/datastore/src/main/java/com/team/bottles/core/datastore/di/LocalDataSourceModule.kt rename to core/datastore/src/main/java/com/team/bottles/core/datastore/di/DataStoreDataSourceModule.kt index b04e1a81..76bab67b 100644 --- a/core/datastore/src/main/java/com/team/bottles/core/datastore/di/LocalDataSourceModule.kt +++ b/core/datastore/src/main/java/com/team/bottles/core/datastore/di/DataStoreDataSourceModule.kt @@ -9,7 +9,7 @@ import dagger.hilt.components.SingletonComponent @Module @InstallIn(SingletonComponent::class) -abstract class LocalDataSourceModule { +abstract class DataStoreDataSourceModule { @Binds abstract fun bindsTokenDataSource(dataSourceImpl: TokenDataSourceImpl): TokenDataSource diff --git a/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/components/bars/BottomBar.kt b/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/components/bars/BottomBar.kt index 10d3079b..788fc4ea 100644 --- a/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/components/bars/BottomBar.kt +++ b/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/components/bars/BottomBar.kt @@ -3,6 +3,7 @@ package com.team.bottles.core.designsystem.components.bars import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -18,16 +19,14 @@ fun BottlesBottomBar( modifier: Modifier = Modifier, text: String, onClick: () -> Unit, - enabled: Boolean = false + enabled: Boolean = false, + isDebounce: Boolean = true, ) { Box( modifier = modifier + .height(height = 88.dp) .background(brush = BottlesTheme.color.background.tertiary) - .padding( - top = 24.dp, - start = 16.dp, - end = 16.dp - ), + .padding(horizontal = BottlesTheme.spacing.medium), contentAlignment = Alignment.BottomCenter ) { BottlesSolidButton( @@ -36,7 +35,7 @@ fun BottlesBottomBar( text = text, enabled = enabled, onClick = onClick, - isDebounce = true, + isDebounce = isDebounce, ) } } diff --git a/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/components/bars/TopBar.kt b/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/components/bars/TopBar.kt index 9a6c26fb..2e950b60 100644 --- a/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/components/bars/TopBar.kt +++ b/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/components/bars/TopBar.kt @@ -29,12 +29,13 @@ fun BottlesTopBar( Box( modifier = modifier .fillMaxWidth() - .height(48.dp) - .padding(horizontal = 16.dp), + .height(48.dp), contentAlignment = Alignment.Center, ) { Row( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = BottlesTheme.spacing.medium), horizontalArrangement = if (leadingIcon == null) Arrangement.End else Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { diff --git a/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/components/buttons/IconButton.kt b/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/components/buttons/IconButton.kt index 23716d06..2f048b21 100644 --- a/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/components/buttons/IconButton.kt +++ b/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/components/buttons/IconButton.kt @@ -8,11 +8,13 @@ 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.shape.CircleShape import androidx.compose.material3.Icon import androidx.compose.material3.Text 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.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -39,13 +41,14 @@ fun BottlesIconButtonWithText( color = BottlesTheme.color.border.enabled, shape = BottlesTheme.shape.extraSmall ) - .padding( - horizontal = BottlesTheme.spacing.small, - vertical = 7.5f.dp - ) + .clip(shape = BottlesTheme.shape.extraSmall) .noRippleClickable( onClick = onClick, enabled = enabled + ) + .padding( + horizontal = BottlesTheme.spacing.small, + vertical = 7.5f.dp ), horizontalArrangement = Arrangement.spacedBy( space = BottlesTheme.spacing.doubleExtraSmall, @@ -66,29 +69,46 @@ fun BottlesIconButtonWithText( } } +enum class IconButtonType { + CIRCLE, + RECTANGLE, + ; +} + @Composable fun BottlesIconButton( modifier: Modifier = Modifier, + iconButtonType: IconButtonType, @DrawableRes icon: Int, onClick: () -> Unit, enabled: Boolean = true ) { + val shape = when (iconButtonType) { + IconButtonType.RECTANGLE -> BottlesTheme.shape.extraSmall + IconButtonType.CIRCLE -> CircleShape + } + val padding = when (iconButtonType) { + IconButtonType.RECTANGLE -> 10.dp + IconButtonType.CIRCLE -> 6.dp + } + Box( modifier = modifier .background( color = BottlesTheme.color.container.enabledPrimary, - shape = BottlesTheme.shape.extraSmall + shape = shape ) .border( width = 1.dp, color = BottlesTheme.color.border.enabled, - shape = BottlesTheme.shape.extraSmall + shape = shape ) - .padding(10.dp) + .clip(shape = shape) .noRippleClickable( onClick = onClick, enabled = enabled ) + .padding(all = padding) ) { Icon( painter = painterResource(id = icon), @@ -98,13 +118,14 @@ fun BottlesIconButton( } } -@Preview +@Preview(showBackground = true) @Composable private fun BottlesIconButtonPreview() { BottlesTheme { - Column { + Column(verticalArrangement = Arrangement.spacedBy(3.dp)) { BottlesIconButton( icon = R.drawable.ic_close_16, + iconButtonType = IconButtonType.RECTANGLE, onClick = {} ) BottlesIconButtonWithText( @@ -112,6 +133,11 @@ private fun BottlesIconButtonPreview() { icon = R.drawable.ic_group_14, onClick = {} ) + BottlesIconButton( + icon = R.drawable.ic_pencil_12, + iconButtonType = IconButtonType.CIRCLE, + onClick = {} + ) } } } \ No newline at end of file diff --git a/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/components/buttons/SolidButton.kt b/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/components/buttons/SolidButton.kt index d6a3f984..b0cde580 100644 --- a/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/components/buttons/SolidButton.kt +++ b/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/components/buttons/SolidButton.kt @@ -17,6 +17,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp @@ -25,8 +26,7 @@ import com.team.bottles.core.designsystem.modifier.debounceClickable import com.team.bottles.core.designsystem.theme.BottlesTheme enum class SolidButtonType(val height: Dp) { - XS(height = 36.dp), - SM(height = 56.dp), + SM(height = 36.dp), MD(height = 56.dp), LG(height = 64.dp), ; @@ -46,8 +46,15 @@ fun BottlesSolidButton( val isPressed by interactionSource.collectIsPressedAsState() val shape = when(buttonType) { - SolidButtonType.XS -> BottlesTheme.shape.extraSmall - else -> BottlesTheme.shape.small + SolidButtonType.SM -> BottlesTheme.shape.extraSmall + SolidButtonType.MD -> BottlesTheme.shape.small + SolidButtonType.LG -> BottlesTheme.shape.medium + } + + val backgroundColor = when { + !enabled -> BottlesTheme.color.container.disabledSecondary + isPressed -> BottlesTheme.color.container.pressed + else -> BottlesTheme.color.container.enabledSecondary } SolidButton( @@ -55,9 +62,9 @@ fun BottlesSolidButton( onClick = onClick, enabled = enabled, shape = shape, + backgroundColor = backgroundColor, isDebounce = isDebounce, interactionSource = interactionSource, - isPressed = isPressed, buttonType = buttonType, contentHorizontalPadding = contentHorizontalPadding ) { @@ -68,7 +75,7 @@ fun BottlesSolidButton( } val textStyle = when(buttonType) { - SolidButtonType.XS -> BottlesTheme.typography.body + SolidButtonType.SM -> BottlesTheme.typography.body else -> BottlesTheme.typography.subTitle1 } @@ -86,19 +93,13 @@ fun SolidButton( onClick: () -> Unit, enabled: Boolean, shape: Shape, + backgroundColor: Color, buttonType: SolidButtonType, contentHorizontalPadding: Dp, isDebounce: Boolean, interactionSource: MutableInteractionSource, - isPressed: Boolean, content: @Composable () -> Unit, ) { - val backgroundColor = when { - !enabled -> BottlesTheme.color.container.disabledSecondary - isPressed -> BottlesTheme.color.container.pressed - else -> BottlesTheme.color.container.enabledSecondary - } - Box( modifier = modifier .height(height = buttonType.height) @@ -107,11 +108,6 @@ fun SolidButton( shape = shape ) .clip(shape = shape) - .padding( - paddingValues = PaddingValues( - horizontal = contentHorizontalPadding, - ), - ) .then( if (isDebounce) { Modifier.debounceClickable( @@ -128,6 +124,11 @@ fun SolidButton( indication = null ) } + ) + .padding( + paddingValues = PaddingValues( + horizontal = contentHorizontalPadding, + ), ), contentAlignment = Alignment.Center ) { @@ -142,12 +143,6 @@ fun SolidButton( private fun SolidButtonPreview() { BottlesTheme { Column(verticalArrangement = Arrangement.spacedBy(3.dp)) { - BottlesSolidButton( - buttonType = SolidButtonType.XS, - text = "Text", - onClick = { }, - contentHorizontalPadding = 12.dp - ) BottlesSolidButton( buttonType = SolidButtonType.SM, text = "Text", diff --git a/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/components/cards/Card.kt b/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/components/cards/Card.kt new file mode 100644 index 00000000..e59278d9 --- /dev/null +++ b/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/components/cards/Card.kt @@ -0,0 +1,63 @@ +package com.team.bottles.core.designsystem.components.cards + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.team.bottles.core.designsystem.components.lists.BottlesSettingItemWithArrow +import com.team.bottles.core.designsystem.theme.BottlesTheme + +@Composable +fun BottlesCard( + modifier: Modifier = Modifier, + verticalArrangement: Arrangement.Vertical = Arrangement.Top, + contents: @Composable () -> Unit +) { + Column( + modifier = modifier + .background( + color = BottlesTheme.color.container.primary, + shape = BottlesTheme.shape.extraLarge + ) + .border( + width = 1.dp, + shape = BottlesTheme.shape.extraLarge, + color = BottlesTheme.color.border.primary + ) + .clip(shape = BottlesTheme.shape.extraLarge) + .padding( + vertical = BottlesTheme.spacing.extraLarge, + horizontal = BottlesTheme.spacing.medium + ), + verticalArrangement = verticalArrangement + ) { + contents.invoke() + } +} + +@Preview +@Composable +private fun BottlesCardPreview() { + BottlesTheme { + BottlesCard( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy( + space = BottlesTheme.spacing.extraLarge + ) + ) { + BottlesSettingItemWithArrow( + title = "프로필 수정", + onClickItem = {} + ) + } + } +} \ No newline at end of file diff --git a/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/components/etc/Profile.kt b/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/components/etc/Profile.kt new file mode 100644 index 00000000..477740ea --- /dev/null +++ b/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/components/etc/Profile.kt @@ -0,0 +1,143 @@ +package com.team.bottles.core.designsystem.components.etc + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +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.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.skydoves.cloudy.cloudy +import com.skydoves.landscapist.ImageOptions +import com.skydoves.landscapist.coil.CoilImage +import com.team.bottles.core.designsystem.R +import com.team.bottles.core.designsystem.modifier.debounceNoRippleClickable +import com.team.bottles.core.designsystem.theme.BottlesTheme + +enum class ProfileImageType(val size: Dp) { + SM(size = 40.dp), + LG(size = 80.dp), + ; +} + +@Composable +fun BottlesProfileEdit( + modifier: Modifier = Modifier, + onClickImage: () -> Unit, + imageUrl: String, + profileImageType: ProfileImageType = ProfileImageType.LG, +) { + Box( + modifier = modifier + .debounceNoRippleClickable(onClick = onClickImage) + ) { + BottlesProfile( + imageUrl = imageUrl, + profileImageType = profileImageType, + isBlur = false + ) + Box( + modifier = modifier + .align(Alignment.BottomEnd) + .offset(x = 5.dp) + .background( + color = BottlesTheme.color.container.enabledPrimary, + shape = CircleShape + ) + .border( + width = 1.dp, + color = BottlesTheme.color.border.enabled, + shape = CircleShape + ) + .clip(shape = CircleShape) + .padding(all = 6.dp), + ) { + Icon( + painter = painterResource(id = R.drawable.ic_pencil_12), + contentDescription = null, + tint = BottlesTheme.color.icon.primary + ) + } + } +} + +@Composable +fun BottlesProfile( + modifier: Modifier = Modifier, + imageUrl: String, + profileImageType: ProfileImageType, + isBlur: Boolean = true +) { + CoilImage( + modifier = modifier + .size(size = profileImageType.size) + .clip(shape = CircleShape) + .then( + if (isBlur) { + Modifier.cloudy(radius = 5) + } else { + Modifier + } + ), + imageModel = { imageUrl }, + previewPlaceholder = painterResource(id = R.drawable.sample_image), + imageOptions = ImageOptions( + contentScale = ContentScale.Crop + ), + loading = { + Box( + modifier = Modifier + .size(80.dp) + .background( + color = BottlesTheme.color.icon.secondary, + shape = CircleShape + ) + .clip(shape = CircleShape) + ) + }, + failure = { + Box( + modifier = Modifier + .size(80.dp) + .background( + color = BottlesTheme.color.icon.secondary, + shape = CircleShape + ) + .clip(shape = CircleShape) + ) + } + ) +} + +@Preview +@Composable +private fun ProfileEditPreview() { + BottlesTheme { + Column(verticalArrangement = Arrangement.spacedBy(3.dp)) { + BottlesProfileEdit( + onClickImage = { /*TODO*/ }, + imageUrl = "" + ) + BottlesProfile( + imageUrl = "", + profileImageType = ProfileImageType.LG + ) + BottlesProfile( + imageUrl = "", + profileImageType = ProfileImageType.SM + ) + } + } +} \ No newline at end of file diff --git a/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/components/etc/Toggle.kt b/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/components/etc/Toggle.kt new file mode 100644 index 00000000..92708123 --- /dev/null +++ b/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/components/etc/Toggle.kt @@ -0,0 +1,78 @@ +package com.team.bottles.core.designsystem.components.etc + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.selection.toggleable +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.team.bottles.core.designsystem.modifier.noRippleClickable +import com.team.bottles.core.designsystem.theme.BottlesTheme + +@Composable +fun BottlesToggleButton( + modifier: Modifier = Modifier, + checked: Boolean, + onCheckedChange: () -> Unit, + enabled: Boolean = true, +) { + val backgroundColor = if (checked) { + BottlesTheme.color.icon.selected + } else { + BottlesTheme.color.icon.disabled + } + val shape = RoundedCornerShape(100.dp) + + Box( + modifier = modifier + .width(44.dp) + .background( + color = backgroundColor, + shape = shape + ) + .clip(shape = shape) + .padding(2.dp) + .noRippleClickable( + onClick = onCheckedChange, + enabled = enabled + ), + contentAlignment = if (checked) Alignment.TopEnd else Alignment.TopStart + ) { + Canvas( + modifier = Modifier + .size(size = 22.dp) + ) { + drawCircle(color = Color.White) + } + } +} + +@Preview +@Composable +private fun BottlesToggleButtonPreview() { + BottlesTheme { + Column(verticalArrangement = Arrangement.spacedBy(3.dp)) { + BottlesToggleButton( + checked = false, + onCheckedChange = {} + ) + BottlesToggleButton( + checked = true, + onCheckedChange = {} + ) + } + } +} \ No newline at end of file diff --git a/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/components/etc/UserInfo.kt b/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/components/etc/UserInfo.kt index fd018d54..1b497528 100644 --- a/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/components/etc/UserInfo.kt +++ b/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/components/etc/UserInfo.kt @@ -4,32 +4,25 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Icon import androidx.compose.material3.Text 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.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.skydoves.cloudy.cloudy -import com.skydoves.landscapist.ImageOptions -import com.skydoves.landscapist.coil.CoilImage import com.team.bottles.core.designsystem.R import com.team.bottles.core.designsystem.theme.BottlesTheme @Composable -fun UserInfo( +fun BottlesUserInfo( modifier: Modifier = Modifier, + onClickImage: () -> Unit, imageUrl: String, userName: String, userAge: Int, - isBlur: Boolean = true + profileImageType: ProfileImageType = ProfileImageType.LG ) { Column( modifier = modifier.fillMaxWidth(), @@ -38,29 +31,64 @@ fun UserInfo( ), horizontalAlignment = Alignment.CenterHorizontally ) { - CoilImage( - modifier = Modifier - .size(80.dp) - .clip(shape = CircleShape) - .then( - if (isBlur) { - Modifier.cloudy(radius = 5) - } else { - Modifier - } - ), - imageModel = { imageUrl }, - previewPlaceholder = painterResource(id = R.drawable.sample_image), - imageOptions = ImageOptions( - contentScale = ContentScale.Crop - ), - loading = { + BottlesProfileEdit( + imageUrl = imageUrl, + profileImageType = profileImageType, + onClickImage = onClickImage + ) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy( + space = BottlesTheme.spacing.extraSmall + ) + ) { + Text( + text = userName, + style = BottlesTheme.typography.subTitle1, + color = BottlesTheme.color.text.secondary + ) - }, - failure = { + Icon( + painter = painterResource(id = R.drawable.ic_spacing_bar_3_15), + contentDescription = null, + tint = BottlesTheme.color.border.secondary + ) + + Text( + text = stringResource( + id = R.string.user_age_user_info, + formatArgs = arrayOf(userAge) + ), + style = BottlesTheme.typography.body, + color = BottlesTheme.color.text.secondary + ) + } + } +} - } +@Composable +fun BottlesUserInfo( + modifier: Modifier = Modifier, + imageUrl: String, + userName: String, + userAge: Int, + isBlur: Boolean = true, + profileImageType: ProfileImageType = ProfileImageType.LG +) { + Column( + modifier = modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy( + space = BottlesTheme.spacing.small + ), + horizontalAlignment = Alignment.CenterHorizontally + ) { + BottlesProfile( + imageUrl = imageUrl, + profileImageType = profileImageType, + isBlur = isBlur ) + Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy( @@ -72,11 +100,13 @@ fun UserInfo( style = BottlesTheme.typography.subTitle1, color = BottlesTheme.color.text.secondary ) + Icon( painter = painterResource(id = R.drawable.ic_spacing_bar_3_15), contentDescription = null, tint = BottlesTheme.color.border.secondary ) + Text( text = stringResource( id = R.string.user_age_user_info, @@ -93,10 +123,23 @@ fun UserInfo( @Composable private fun UserInfoPreview() { BottlesTheme { - UserInfo( + BottlesUserInfo( + imageUrl = "", + userName = "냥냥이", + userAge = 15, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun UserInfoEditPreview() { + BottlesTheme { + BottlesUserInfo( imageUrl = "", userName = "냥냥이", - userAge = 15 + userAge = 15, + onClickImage = {} ) } } \ No newline at end of file diff --git a/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/components/lists/SettingItem.kt b/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/components/lists/SettingItem.kt new file mode 100644 index 00000000..241e4f7f --- /dev/null +++ b/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/components/lists/SettingItem.kt @@ -0,0 +1,226 @@ +package com.team.bottles.core.designsystem.components.lists + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.team.bottles.core.designsystem.R +import com.team.bottles.core.designsystem.components.buttons.BottlesOutLinedButton +import com.team.bottles.core.designsystem.components.buttons.OutlinedButtonType +import com.team.bottles.core.designsystem.components.etc.BottlesToggleButton +import com.team.bottles.core.designsystem.modifier.noRippleClickable +import com.team.bottles.core.designsystem.theme.BottlesTheme + +@Composable +fun BottlesSettingItemWithButton( + modifier: Modifier = Modifier, + title: String, + subTitle: String, + onClickButton: () -> Unit, + buttonText: String, +) { + SettingItem( + modifier = modifier, + leadingTitle = { + SettingItemTitleAndSubTitle(title = title, subTitle = subTitle) + }, + trailingComponent = { + BottlesOutLinedButton( + text = buttonText, + buttonType = OutlinedButtonType.SM, + onClick = onClickButton, + contentHorizontalPadding = BottlesTheme.spacing.small + ) + } + ) +} + +@Composable +fun BottlesSettingItemWithToggleButton( + modifier: Modifier = Modifier, + title: String, + subTitle: String? = null, + checked: Boolean, + onCheckedChange: () -> Unit, + enabled: Boolean = true +) { + SettingItem( + modifier = modifier, + leadingTitle = { + if (subTitle == null) { + SettingItemSingleTitle(title = title) + } else { + SettingItemTitleAndSubTitle(title = title, subTitle = subTitle) + } + }, + trailingComponent = { + BottlesToggleButton( + checked = checked, + onCheckedChange = onCheckedChange, + enabled = enabled + ) + } + ) +} + +@Composable +fun BottlesSettingItemWithArrow( + modifier: Modifier = Modifier, + title: String, + subTitle: String? = null, + onClickItem: () -> Unit +) { + SettingItem( + modifier = modifier.noRippleClickable(onClick = onClickItem), + leadingTitle = { + if (subTitle == null) { + SettingItemSingleTitle(title = title) + } else { + SettingItemTitleAndSubTitle(title = title, subTitle = subTitle) + } + }, + trailingComponent = { + Icon( + painter = painterResource(id = R.drawable.ic_right_16), + contentDescription = null, + tint = BottlesTheme.color.icon.primary + ) + } + ) +} + +@Composable +fun BottlesSettingItem( + modifier: Modifier = Modifier, + title: String, + subTitle: String, +) { + SettingItem( + modifier = modifier, + leadingTitle = { + SettingItemTitleAndSubTitle(title = title, subTitle = subTitle) + }, + ) +} + +@Composable +private fun RowScope.SettingItemSingleTitle( + modifier: Modifier = Modifier, + title: String, +) { + Box( + modifier = modifier + .weight(weight = 1f) + .height(height = 26.dp), + contentAlignment = Alignment.CenterStart + ) { + Text( + text = title, + style = BottlesTheme.typography.subTitle2, + color = BottlesTheme.color.text.secondary + ) + } +} + +@Composable +private fun RowScope.SettingItemTitleAndSubTitle( + modifier: Modifier = Modifier, + title: String, + subTitle: String +) { + Column( + modifier = modifier + .weight(weight = 1f), + verticalArrangement = Arrangement.spacedBy( + space = BottlesTheme.spacing.extraSmall + ) + ) { + Text( + text = title, + style = BottlesTheme.typography.subTitle2, + color = BottlesTheme.color.text.secondary + ) + Text( + modifier = Modifier.fillMaxWidth(), + text = subTitle, + style = BottlesTheme.typography.caption, + color = BottlesTheme.color.text.tertiary + ) + } +} + +@Composable +private fun SettingItem( + modifier: Modifier = Modifier, + leadingTitle: @Composable (RowScope.() -> Unit)? = null, + trailingComponent: @Composable (RowScope.() -> Unit)? = null, +) { + Row( + modifier = modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + if (leadingTitle != null) { + leadingTitle() + } + + if (leadingTitle != null && trailingComponent != null) { + Spacer(modifier = Modifier.width(width = BottlesTheme.spacing.extraSmall)) + } + + if (trailingComponent != null) { + trailingComponent() + } + } +} + +@Preview(showBackground = true) +@Composable +private fun BottlesListTitleAndArrowPreview() { + BottlesTheme { + Column(verticalArrangement = Arrangement.spacedBy(20.dp)) { + BottlesSettingItemWithArrow( + title = "text", + onClickItem = {} + ) + BottlesSettingItemWithArrow( + title = "text", + subTitle = "subText", + onClickItem = {} + ) + BottlesSettingItemWithToggleButton( + title = "text", + checked = true, + onCheckedChange = {} + ) + BottlesSettingItemWithToggleButton( + title = "text", + subTitle = "subText", + checked = true, + onCheckedChange = {} + ) + BottlesSettingItemWithButton( + title = "text", + subTitle = "subText", + onClickButton = { /*TODO*/ }, + buttonText = "Text" + ) + BottlesSettingItem( + title = "text", + subTitle = "subText", + ) + } + } +} \ No newline at end of file diff --git a/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/components/popup/BalloonPopup.kt b/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/components/popup/BalloonPopup.kt index 56c20218..0346f6d3 100644 --- a/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/components/popup/BalloonPopup.kt +++ b/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/components/popup/BalloonPopup.kt @@ -15,13 +15,19 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.team.bottles.core.designsystem.R import com.team.bottles.core.designsystem.components.buttons.BottlesSolidButton import com.team.bottles.core.designsystem.components.buttons.SolidButtonType +import com.team.bottles.core.designsystem.components.etc.chips.BottlesChip import com.team.bottles.core.designsystem.theme.BottlesTheme @Composable @@ -31,17 +37,18 @@ fun BottlesBalloonPopup( ) { val shape = RoundedCornerShape(20.dp) - Column(modifier = modifier) { + Column(modifier = modifier.shadow(4.dp, shape)) { Box( modifier = Modifier + .height(height = 42.dp) .background( color = BottlesTheme.color.container.primary, shape = shape ) .padding( horizontal = BottlesTheme.spacing.large, - vertical = BottlesTheme.spacing.small - ) + ), + contentAlignment = Alignment.Center ) { Text( text = text, @@ -55,7 +62,7 @@ fun BottlesBalloonPopup( .offset(y = (-0.2).dp), painter = painterResource(id = R.drawable.ic_balloon_vertex_10_6), contentDescription = null, - tint = BottlesTheme.color.border.primary + tint = BottlesTheme.color.container.primary ) } } @@ -67,17 +74,20 @@ fun BottlesBalloonPopup( ) { val shape = RoundedCornerShape(20.dp) - Column(modifier = modifier) { + Column( + modifier = modifier.shadow(4.dp, shape) + ) { Box( modifier = Modifier + .height(height = 42.dp) .background( color = BottlesTheme.color.container.primary, shape = shape ) .padding( horizontal = BottlesTheme.spacing.large, - vertical = BottlesTheme.spacing.small - ) + ), + contentAlignment = Alignment.Center ) { Text( text = text, @@ -105,7 +115,7 @@ fun BottlesBalloonPopupWithButton( ) { val shape = RoundedCornerShape(20.dp) - Column(modifier = modifier) { + Column(modifier = modifier.shadow(8.dp, shape)) { Column( modifier = Modifier .background( @@ -122,7 +132,7 @@ fun BottlesBalloonPopupWithButton( ) Spacer(modifier = Modifier.height(height = BottlesTheme.spacing.small)) BottlesSolidButton( - buttonType = SolidButtonType.XS, + buttonType = SolidButtonType.SM, text = buttonText, onClick = onClick, contentHorizontalPadding = 12.dp @@ -139,20 +149,50 @@ fun BottlesBalloonPopupWithButton( } } +@Composable +fun BottlesBalloonPopupWithChip( + modifier: Modifier = Modifier, + text: String, + count: Int +) { + Box(modifier = modifier) { + BottlesBalloonPopup( + text = text + ) + BottlesChip( + modifier = Modifier + .align(Alignment.TopEnd) + .offset(y = (-12).dp), + number = count + ) + } +} -@Preview +@Preview(heightDp = 800, showBackground = true) @Composable private fun BottlesBalloonPopupPreview() { BottlesTheme { Column(verticalArrangement = Arrangement.spacedBy(3.dp)) { BottlesBalloonPopup( - text = "새로운 보틀이 도착했어요!" + text = "보틀을 클릭해 보세요" ) BottlesBalloonPopupWithButton( text = "자기소개 작성 후 열어볼 수 있어요", buttonText = "자기소개 작성하기", onClick = {} ) + BottlesBalloonPopup( + text = buildAnnotatedString { + withStyle(style = SpanStyle(color = Color(0xFF615EFA))) { + append("00") + } + append("시간후 새로운 보틀이 도착해요") + } + ) + BottlesBalloonPopupWithChip( + text = "보틀을 클릭해 보세요", + count = 3 + ) } } } \ No newline at end of file diff --git a/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/foundation/Color.kt b/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/foundation/Color.kt index 1e9aae72..64525c31 100644 --- a/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/foundation/Color.kt +++ b/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/foundation/Color.kt @@ -113,4 +113,5 @@ internal object IconColors { internal val iconSecondary = Neutral200 internal val iconDisabled = Neutral200 internal val iconUpdate = Red + internal val iconSelected = PrimaryPurple500 } \ No newline at end of file diff --git a/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/foundation/Shape.kt b/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/foundation/Shape.kt index 03983726..74bad497 100644 --- a/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/foundation/Shape.kt +++ b/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/foundation/Shape.kt @@ -8,6 +8,7 @@ internal enum class BottlesShapeDefaults(val shape: Shape) { RADIUS_XS(shape = RoundedCornerShape(8.dp)), RADIUS_S(shape = RoundedCornerShape(12.dp)), RADIUS_M(shape = RoundedCornerShape(16.dp)), + RADIUS_L(shape = RoundedCornerShape(20.dp)), RADIUS_XL(shape = RoundedCornerShape(24.dp)), ; } \ No newline at end of file diff --git a/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/theme/BottlesColor.kt b/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/theme/BottlesColor.kt index 5f0813eb..0fc02fef 100644 --- a/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/theme/BottlesColor.kt +++ b/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/theme/BottlesColor.kt @@ -123,5 +123,6 @@ data class Icon( val primary: Color = IconColors.iconPrimary, val secondary: Color = IconColors.iconSecondary, val disabled: Color = IconColors.iconDisabled, - val update: Color = IconColors.iconUpdate + val update: Color = IconColors.iconUpdate, + val selected: Color = IconColors.iconSelected ) diff --git a/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/theme/BottlesShape.kt b/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/theme/BottlesShape.kt index 2b9b033e..b164a37c 100644 --- a/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/theme/BottlesShape.kt +++ b/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/theme/BottlesShape.kt @@ -21,6 +21,7 @@ data class BottlesShape( val extraSmall: Shape, val small: Shape, val medium: Shape, + val large: Shape, val extraLarge: Shape, ) { companion object { @@ -28,6 +29,7 @@ data class BottlesShape( extraSmall = BottlesShapeDefaults.RADIUS_XS.shape, small = BottlesShapeDefaults.RADIUS_S.shape, medium = BottlesShapeDefaults.RADIUS_M.shape, + large = BottlesShapeDefaults.RADIUS_L.shape, extraLarge = BottlesShapeDefaults.RADIUS_XL.shape, ) } diff --git a/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/theme/BottlesTypography.kt b/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/theme/BottlesTypography.kt index aefa93d2..da4b6ca5 100644 --- a/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/theme/BottlesTypography.kt +++ b/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/theme/BottlesTypography.kt @@ -39,7 +39,7 @@ data class BottlesTypography( fontWeight = FontWeight.Bold, fontSize = 32.sp, letterSpacing = 0.sp, - lineHeight = 24.sp * 1.3f, + lineHeight = 41.6f.sp, ), title2 = TextStyle( @@ -47,7 +47,7 @@ data class BottlesTypography( fontWeight = FontWeight.Bold, fontSize = 24.sp, letterSpacing = 0.sp, - lineHeight = 20.sp * 1.3f, + lineHeight = 31.2f.sp, ), title3 = TextStyle( @@ -55,7 +55,7 @@ data class BottlesTypography( fontWeight = FontWeight.Bold, fontSize = 20.sp, letterSpacing = 0.sp, - lineHeight = 20.sp * 1.3f, + lineHeight = 26.sp, ), subTitle1 = TextStyle( @@ -63,7 +63,7 @@ data class BottlesTypography( fontWeight = FontWeight.SemiBold, fontSize = 16.sp, letterSpacing = 0.sp, - lineHeight = 16.sp * 1.3f, + lineHeight = 20.8f.sp, ), subTitle2 = TextStyle( @@ -71,7 +71,7 @@ data class BottlesTypography( fontWeight = FontWeight.SemiBold, fontSize = 14.sp, letterSpacing = 0.sp, - lineHeight = 14.sp * 1.3f, + lineHeight = 18.2f.sp, ), body = TextStyle( @@ -79,7 +79,7 @@ data class BottlesTypography( fontWeight = FontWeight.Medium, fontSize = 14.sp, letterSpacing = 0.sp, - lineHeight = 14.sp * 1.5f, + lineHeight = 21.sp, ), caption = TextStyle( @@ -87,7 +87,7 @@ data class BottlesTypography( fontWeight = FontWeight.Medium, fontSize = 12.sp, letterSpacing = 0.sp, - lineHeight = 12.sp * 1.5f, + lineHeight = 18.sp, ), kakaoLogin = TextStyle( @@ -95,7 +95,7 @@ data class BottlesTypography( fontWeight = FontWeight.Medium, fontSize = 14.sp, letterSpacing = 0.15.sp, - lineHeight = 14.sp * 1.4f, + lineHeight = 19.6f.sp, ), ) } @@ -112,8 +112,8 @@ private fun TypographyPreview() { ) { Text( modifier = Modifier.border(1.dp, Color.Blue), - text = "진심을 담은 보틀로\n서로를 밀도있게 알아가요", - style = BottlesTheme.typography.title3, + text = "아직 보틀을\n찾지 못했어요", + style = BottlesTheme.typography.title1, color = Color.Black, textAlign = TextAlign.Center ) diff --git a/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/util/BottlesIcons.kt b/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/util/BottlesIcons.kt index 47f1bcb3..eb7050bc 100644 --- a/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/util/BottlesIcons.kt +++ b/core/design-system/src/main/kotlin/com/team/bottles/core/designsystem/util/BottlesIcons.kt @@ -18,4 +18,6 @@ object BottlesIcons { val ic_beach_32 = R.drawable.ic_beach_32 val ic_close_16 = R.drawable.ic_close_16 val ic_spacing_bar_3_15 = R.drawable.ic_spacing_bar_3_15 + val ic_pencil_12 = R.drawable.ic_pencil_12 + val ic_warning_24 = R.drawable.ic_warning_24 } diff --git a/core/design-system/src/main/res/drawable/ic_pencil_12.xml b/core/design-system/src/main/res/drawable/ic_pencil_12.xml new file mode 100644 index 00000000..7e481c3e --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_pencil_12.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/core/design-system/src/main/res/drawable/ic_warning_24.xml b/core/design-system/src/main/res/drawable/ic_warning_24.xml new file mode 100644 index 00000000..1e56a5f6 --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_warning_24.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/core/domain/src/main/kotlin/com/team/bottles/core/domain/auth/repository/AuthRepository.kt b/core/domain/src/main/kotlin/com/team/bottles/core/domain/auth/repository/AuthRepository.kt index 4129d09a..057f6b3e 100644 --- a/core/domain/src/main/kotlin/com/team/bottles/core/domain/auth/repository/AuthRepository.kt +++ b/core/domain/src/main/kotlin/com/team/bottles/core/domain/auth/repository/AuthRepository.kt @@ -21,4 +21,8 @@ interface AuthRepository { suspend fun getSavedLocalFcmToken(): String + suspend fun getLatestAppVersion(): Int + + suspend fun getRequiredAppVersion(): Int + } \ No newline at end of file diff --git a/core/domain/src/main/kotlin/com/team/bottles/core/domain/auth/usecase/GetLatestAppVersionUseCase.kt b/core/domain/src/main/kotlin/com/team/bottles/core/domain/auth/usecase/GetLatestAppVersionUseCase.kt new file mode 100644 index 00000000..6fd17e4e --- /dev/null +++ b/core/domain/src/main/kotlin/com/team/bottles/core/domain/auth/usecase/GetLatestAppVersionUseCase.kt @@ -0,0 +1,19 @@ +package com.team.bottles.core.domain.auth.usecase + +import com.team.bottles.core.domain.auth.repository.AuthRepository +import javax.inject.Inject + +class GetLatestAppVersionUseCaseImpl @Inject constructor( + private val authRepository: AuthRepository, +): GetLatestAppVersionUseCase { + + override suspend fun invoke(): Int = + authRepository.getLatestAppVersion() + +} + +interface GetLatestAppVersionUseCase { + + suspend operator fun invoke(): Int + +} \ No newline at end of file diff --git a/core/domain/src/main/kotlin/com/team/bottles/core/domain/auth/usecase/GetRequiredAppVersionUseCase.kt b/core/domain/src/main/kotlin/com/team/bottles/core/domain/auth/usecase/GetRequiredAppVersionUseCase.kt new file mode 100644 index 00000000..40f5c898 --- /dev/null +++ b/core/domain/src/main/kotlin/com/team/bottles/core/domain/auth/usecase/GetRequiredAppVersionUseCase.kt @@ -0,0 +1,19 @@ +package com.team.bottles.core.domain.auth.usecase + +import com.team.bottles.core.domain.auth.repository.AuthRepository +import javax.inject.Inject + +class GetRequiredAppVersionUseCaseImpl @Inject constructor( + private val authRepository: AuthRepository, +) : GetRequiredAppVersionUseCase { + + override suspend fun invoke(): Int = + authRepository.getRequiredAppVersion() + +} + +interface GetRequiredAppVersionUseCase { + + suspend operator fun invoke(): Int + +} \ No newline at end of file diff --git a/core/domain/src/main/kotlin/com/team/bottles/core/domain/user/model/Notification.kt b/core/domain/src/main/kotlin/com/team/bottles/core/domain/user/model/Notification.kt new file mode 100644 index 00000000..5e5392b3 --- /dev/null +++ b/core/domain/src/main/kotlin/com/team/bottles/core/domain/user/model/Notification.kt @@ -0,0 +1,14 @@ +package com.team.bottles.core.domain.user.model + +data class Notification( + val notificationType: NotificationType, + val enabled: Boolean +) + +enum class NotificationType { + DAILY_RANDOM, + RECEIVE_LIKE, + PING_PONG, + MARKETING, + ; +} \ No newline at end of file diff --git a/core/domain/src/main/kotlin/com/team/bottles/core/domain/user/repository/UserRepository.kt b/core/domain/src/main/kotlin/com/team/bottles/core/domain/user/repository/UserRepository.kt index 0eaa25a4..0dc9b08c 100644 --- a/core/domain/src/main/kotlin/com/team/bottles/core/domain/user/repository/UserRepository.kt +++ b/core/domain/src/main/kotlin/com/team/bottles/core/domain/user/repository/UserRepository.kt @@ -1,5 +1,7 @@ package com.team.bottles.core.domain.user.repository +import com.team.bottles.core.domain.user.model.Notification + interface UserRepository { suspend fun reportUser( @@ -7,4 +9,12 @@ interface UserRepository { contents: String ) + suspend fun loadContacts(): List + + suspend fun updateBlockingContacts(contacts: List) + + suspend fun loadSettingNotifications(): List + + suspend fun updateSettingNotification(notification: Notification) + } \ No newline at end of file diff --git a/core/domain/src/main/kotlin/com/team/bottles/core/domain/user/usecase/GetContactsUseCase.kt b/core/domain/src/main/kotlin/com/team/bottles/core/domain/user/usecase/GetContactsUseCase.kt new file mode 100644 index 00000000..024db075 --- /dev/null +++ b/core/domain/src/main/kotlin/com/team/bottles/core/domain/user/usecase/GetContactsUseCase.kt @@ -0,0 +1,19 @@ +package com.team.bottles.core.domain.user.usecase + +import com.team.bottles.core.domain.user.repository.UserRepository +import javax.inject.Inject + +class GetContactsUseCaseImpl @Inject constructor( + private val userRepository: UserRepository, +): GetContactsUseCase { + + override suspend fun invoke(): List = + userRepository.loadContacts() + +} + +interface GetContactsUseCase { + + suspend operator fun invoke(): List + +} \ No newline at end of file diff --git a/core/domain/src/main/kotlin/com/team/bottles/core/domain/user/usecase/GetSettingNotificationsUseCase.kt b/core/domain/src/main/kotlin/com/team/bottles/core/domain/user/usecase/GetSettingNotificationsUseCase.kt new file mode 100644 index 00000000..a6081678 --- /dev/null +++ b/core/domain/src/main/kotlin/com/team/bottles/core/domain/user/usecase/GetSettingNotificationsUseCase.kt @@ -0,0 +1,20 @@ +package com.team.bottles.core.domain.user.usecase + +import com.team.bottles.core.domain.user.model.Notification +import com.team.bottles.core.domain.user.repository.UserRepository +import javax.inject.Inject + +class GetSettingNotificationsUseCaseImpl @Inject constructor( + private val userRepository: UserRepository, +) : GetSettingNotificationsUseCase { + + override suspend fun invoke(): List = + userRepository.loadSettingNotifications() + +} + +interface GetSettingNotificationsUseCase { + + suspend operator fun invoke(): List + +} \ No newline at end of file diff --git a/core/domain/src/main/kotlin/com/team/bottles/core/domain/user/usecase/UpdateBlockingContactsUseCase.kt b/core/domain/src/main/kotlin/com/team/bottles/core/domain/user/usecase/UpdateBlockingContactsUseCase.kt new file mode 100644 index 00000000..4882c041 --- /dev/null +++ b/core/domain/src/main/kotlin/com/team/bottles/core/domain/user/usecase/UpdateBlockingContactsUseCase.kt @@ -0,0 +1,20 @@ +package com.team.bottles.core.domain.user.usecase + +import com.team.bottles.core.domain.user.repository.UserRepository +import javax.inject.Inject + +class UpdateBlockingContactsUseCaseImpl @Inject constructor( + private val userRepository: UserRepository, +): UpdateBlockingContactsUseCase { + + override suspend fun invoke(contacts: List) { + userRepository.updateBlockingContacts(contacts = contacts) + } + +} + +interface UpdateBlockingContactsUseCase { + + suspend operator fun invoke(contacts: List) + +} \ No newline at end of file diff --git a/core/domain/src/main/kotlin/com/team/bottles/core/domain/user/usecase/UpdateSettingNotificationUseCase.kt b/core/domain/src/main/kotlin/com/team/bottles/core/domain/user/usecase/UpdateSettingNotificationUseCase.kt new file mode 100644 index 00000000..944f2ceb --- /dev/null +++ b/core/domain/src/main/kotlin/com/team/bottles/core/domain/user/usecase/UpdateSettingNotificationUseCase.kt @@ -0,0 +1,21 @@ +package com.team.bottles.core.domain.user.usecase + +import com.team.bottles.core.domain.user.model.Notification +import com.team.bottles.core.domain.user.repository.UserRepository +import javax.inject.Inject + +class UpdateSettingNotificationUseCaseImpl @Inject constructor( + private val userRepository: UserRepository, +): UpdateSettingNotificationUseCase { + + override suspend fun invoke(notification: Notification) { + userRepository.updateSettingNotification(notification = notification) + } + +} + +interface UpdateSettingNotificationUseCase { + + suspend operator fun invoke(notification: Notification) + +} \ No newline at end of file diff --git a/core/local/.gitignore b/core/local/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/local/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/local/build.gradle.kts b/core/local/build.gradle.kts new file mode 100644 index 00000000..de030653 --- /dev/null +++ b/core/local/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + id("team.bottles.android.library") + id("team.bottles.android.hilt") +} + +android { + namespace = "com.team.bottles.local" +} + +dependencies { + +} \ No newline at end of file diff --git a/core/local/src/main/kotlin/com/team/bottles/local/datasource/DeviceDataSource.kt b/core/local/src/main/kotlin/com/team/bottles/local/datasource/DeviceDataSource.kt new file mode 100644 index 00000000..38ab631b --- /dev/null +++ b/core/local/src/main/kotlin/com/team/bottles/local/datasource/DeviceDataSource.kt @@ -0,0 +1,7 @@ +package com.team.bottles.local.datasource + +interface DeviceDataSource { + + suspend fun getContacts(): List + +} \ No newline at end of file diff --git a/core/local/src/main/kotlin/com/team/bottles/local/datasource/DeviceDataSourceImpl.kt b/core/local/src/main/kotlin/com/team/bottles/local/datasource/DeviceDataSourceImpl.kt new file mode 100644 index 00000000..6d5e4973 --- /dev/null +++ b/core/local/src/main/kotlin/com/team/bottles/local/datasource/DeviceDataSourceImpl.kt @@ -0,0 +1,37 @@ +package com.team.bottles.local.datasource + +import android.content.ContentResolver +import android.content.Context +import android.database.Cursor +import android.provider.ContactsContract +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +class DeviceDataSourceImpl @Inject constructor( + @ApplicationContext private val context: Context, +) : DeviceDataSource { + + override suspend fun getContacts(): List { + val contacts = mutableSetOf() + val contentResolver: ContentResolver = context.contentResolver + val cursor: Cursor? = contentResolver.query( + ContactsContract.CommonDataKinds.Phone.CONTENT_URI, + null, + null, + null, + null + ) + + cursor?.use { + val numberIndex = it.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER) + + while (it.moveToNext()) { + val number = it.getString(numberIndex).replace(Regex("[^0-9]"), "") + contacts.add(number) + } + } + + return contacts.toList() + } + +} \ No newline at end of file diff --git a/core/local/src/main/kotlin/com/team/bottles/local/di/LocalDataSourceModule.kt b/core/local/src/main/kotlin/com/team/bottles/local/di/LocalDataSourceModule.kt new file mode 100644 index 00000000..795d7e1c --- /dev/null +++ b/core/local/src/main/kotlin/com/team/bottles/local/di/LocalDataSourceModule.kt @@ -0,0 +1,17 @@ +package com.team.bottles.local.di + +import com.team.bottles.local.datasource.DeviceDataSource +import com.team.bottles.local.datasource.DeviceDataSourceImpl +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +abstract class LocalDataSourceModule { + + @Binds + abstract fun bindsDeviceDataSource(dataSourceImpl: DeviceDataSourceImpl): DeviceDataSource + +} \ No newline at end of file diff --git a/core/navigator/src/main/kotlin/SettingNavigator.kt b/core/navigator/src/main/kotlin/SettingNavigator.kt new file mode 100644 index 00000000..334b6f4a --- /dev/null +++ b/core/navigator/src/main/kotlin/SettingNavigator.kt @@ -0,0 +1,11 @@ +import kotlinx.serialization.Serializable + +sealed interface SettingNavigator { + + @Serializable + data object Notification : SettingNavigator + + @Serializable + data object Account : SettingNavigator + +} diff --git a/core/network/src/main/kotlin/com/team/bottles/network/api/AuthService.kt b/core/network/src/main/kotlin/com/team/bottles/network/api/AuthService.kt index d996bed2..0dee2bfa 100644 --- a/core/network/src/main/kotlin/com/team/bottles/network/api/AuthService.kt +++ b/core/network/src/main/kotlin/com/team/bottles/network/api/AuthService.kt @@ -8,7 +8,9 @@ import com.team.bottles.network.dto.auth.request.SignUpRequest import com.team.bottles.network.dto.auth.request.SmsSignInRequest import com.team.bottles.network.dto.auth.response.KakaoSignInUpResponse import com.team.bottles.network.dto.auth.response.TokensResponse +import com.team.bottles.network.dto.auth.response.UpdateAppVersionResponse import retrofit2.http.Body +import retrofit2.http.GET import retrofit2.http.Header import retrofit2.http.POST @@ -61,4 +63,7 @@ interface AuthService { @Body fcmUpdateRequest: FcmUpdateRequest ) + @GET("/api/v1/auth/app-version") + suspend fun getRequiredMinimumAppVersion(): UpdateAppVersionResponse + } diff --git a/core/network/src/main/kotlin/com/team/bottles/network/api/UserService.kt b/core/network/src/main/kotlin/com/team/bottles/network/api/UserService.kt index 2e67fae8..ca263de5 100644 --- a/core/network/src/main/kotlin/com/team/bottles/network/api/UserService.kt +++ b/core/network/src/main/kotlin/com/team/bottles/network/api/UserService.kt @@ -1,7 +1,11 @@ package com.team.bottles.network.api +import com.team.bottles.network.dto.auth.request.BlockContactListRequest +import com.team.bottles.network.dto.user.request.AlimyOnOffRequest import com.team.bottles.network.dto.user.request.ReportUserRequest +import com.team.bottles.network.dto.user.response.AlimyResponse import retrofit2.http.Body +import retrofit2.http.GET import retrofit2.http.POST interface UserService { @@ -11,4 +15,17 @@ interface UserService { @Body request: ReportUserRequest ) + @POST("/api/v1/user/block/contact-list") + suspend fun postBlockedContacts( + @Body request: BlockContactListRequest + ) + + @GET("/api/v1/user/alimy") + suspend fun getSettingNotifications(): List + + @POST("/api/v1/user/alimy") + suspend fun postSettingNotification( + @Body request: AlimyOnOffRequest + ) + } \ No newline at end of file diff --git a/core/network/src/main/kotlin/com/team/bottles/network/datasource/AuthDataSource.kt b/core/network/src/main/kotlin/com/team/bottles/network/datasource/AuthDataSource.kt index 928afcbf..2013fecf 100644 --- a/core/network/src/main/kotlin/com/team/bottles/network/datasource/AuthDataSource.kt +++ b/core/network/src/main/kotlin/com/team/bottles/network/datasource/AuthDataSource.kt @@ -8,6 +8,7 @@ import com.team.bottles.network.dto.auth.request.SignUpRequest import com.team.bottles.network.dto.auth.request.SmsSignInRequest import com.team.bottles.network.dto.auth.response.KakaoSignInUpResponse import com.team.bottles.network.dto.auth.response.TokensResponse +import com.team.bottles.network.dto.auth.response.UpdateAppVersionResponse interface AuthDataSource { @@ -41,4 +42,6 @@ interface AuthDataSource { request: FcmUpdateRequest ) + suspend fun fetchRequiredMinimumAppVersion(): UpdateAppVersionResponse + } \ No newline at end of file diff --git a/core/network/src/main/kotlin/com/team/bottles/network/datasource/AuthDataSourceImpl.kt b/core/network/src/main/kotlin/com/team/bottles/network/datasource/AuthDataSourceImpl.kt index 62dc5ee0..54ad6c2a 100644 --- a/core/network/src/main/kotlin/com/team/bottles/network/datasource/AuthDataSourceImpl.kt +++ b/core/network/src/main/kotlin/com/team/bottles/network/datasource/AuthDataSourceImpl.kt @@ -9,6 +9,7 @@ import com.team.bottles.network.dto.auth.request.SignUpRequest import com.team.bottles.network.dto.auth.request.SmsSignInRequest import com.team.bottles.network.dto.auth.response.KakaoSignInUpResponse import com.team.bottles.network.dto.auth.response.TokensResponse +import com.team.bottles.network.dto.auth.response.UpdateAppVersionResponse import javax.inject.Inject class AuthDataSourceImpl @Inject constructor( @@ -47,6 +48,9 @@ class AuthDataSourceImpl @Inject constructor( authService.postUpdatedFcmToken(accessToken = "$TOKEN_TYPE $accessToken", fcmUpdateRequest = request) } + override suspend fun fetchRequiredMinimumAppVersion(): UpdateAppVersionResponse = + authService.getRequiredMinimumAppVersion() + companion object { private const val TOKEN_TYPE = "Bearer" } diff --git a/core/network/src/main/kotlin/com/team/bottles/network/datasource/UserDataSource.kt b/core/network/src/main/kotlin/com/team/bottles/network/datasource/UserDataSource.kt index 8e1df838..68785d82 100644 --- a/core/network/src/main/kotlin/com/team/bottles/network/datasource/UserDataSource.kt +++ b/core/network/src/main/kotlin/com/team/bottles/network/datasource/UserDataSource.kt @@ -1,9 +1,18 @@ package com.team.bottles.network.datasource +import com.team.bottles.network.dto.auth.request.BlockContactListRequest +import com.team.bottles.network.dto.user.request.AlimyOnOffRequest import com.team.bottles.network.dto.user.request.ReportUserRequest +import com.team.bottles.network.dto.user.response.AlimyResponse interface UserDataSource { suspend fun sendReportContents(request: ReportUserRequest) + suspend fun updateWantToBlockContacts(request: BlockContactListRequest) + + suspend fun fetchSettingNotifications(): List + + suspend fun updateSettingNotification(request: AlimyOnOffRequest) + } \ No newline at end of file diff --git a/core/network/src/main/kotlin/com/team/bottles/network/datasource/UserDataSourceImpl.kt b/core/network/src/main/kotlin/com/team/bottles/network/datasource/UserDataSourceImpl.kt index bd79d645..370e8b33 100644 --- a/core/network/src/main/kotlin/com/team/bottles/network/datasource/UserDataSourceImpl.kt +++ b/core/network/src/main/kotlin/com/team/bottles/network/datasource/UserDataSourceImpl.kt @@ -1,7 +1,10 @@ package com.team.bottles.network.datasource import com.team.bottles.network.api.UserService +import com.team.bottles.network.dto.auth.request.BlockContactListRequest +import com.team.bottles.network.dto.user.request.AlimyOnOffRequest import com.team.bottles.network.dto.user.request.ReportUserRequest +import com.team.bottles.network.dto.user.response.AlimyResponse import javax.inject.Inject class UserDataSourceImpl @Inject constructor( @@ -12,4 +15,15 @@ class UserDataSourceImpl @Inject constructor( userService.postReportUser(request = request) } + override suspend fun updateWantToBlockContacts(request: BlockContactListRequest) { + userService.postBlockedContacts(request = request) + } + + override suspend fun fetchSettingNotifications(): List = + userService.getSettingNotifications() + + override suspend fun updateSettingNotification(request: AlimyOnOffRequest) { + userService.postSettingNotification(request = request) + } + } \ No newline at end of file diff --git a/core/network/src/main/kotlin/com/team/bottles/network/di/NetworkModule.kt b/core/network/src/main/kotlin/com/team/bottles/network/di/NetworkModule.kt index 278f53c2..e401d74c 100644 --- a/core/network/src/main/kotlin/com/team/bottles/network/di/NetworkModule.kt +++ b/core/network/src/main/kotlin/com/team/bottles/network/di/NetworkModule.kt @@ -31,6 +31,7 @@ object NetworkModule { fun provideJson(): Json = Json { coerceInputValues = true prettyPrint = true + ignoreUnknownKeys = true } @Singleton diff --git a/core/network/src/main/kotlin/com/team/bottles/network/dto/auth/request/BlockContactListRequest.kt b/core/network/src/main/kotlin/com/team/bottles/network/dto/auth/request/BlockContactListRequest.kt new file mode 100644 index 00000000..c053c642 --- /dev/null +++ b/core/network/src/main/kotlin/com/team/bottles/network/dto/auth/request/BlockContactListRequest.kt @@ -0,0 +1,9 @@ +package com.team.bottles.network.dto.auth.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class BlockContactListRequest( + @SerialName("blockContacts") val blockContacts: List +) \ No newline at end of file diff --git a/core/network/src/main/kotlin/com/team/bottles/network/dto/auth/response/UpdateAppVersionResponse.kt b/core/network/src/main/kotlin/com/team/bottles/network/dto/auth/response/UpdateAppVersionResponse.kt new file mode 100644 index 00000000..a19e40e0 --- /dev/null +++ b/core/network/src/main/kotlin/com/team/bottles/network/dto/auth/response/UpdateAppVersionResponse.kt @@ -0,0 +1,10 @@ +package com.team.bottles.network.dto.auth.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class UpdateAppVersionResponse( + @SerialName("minimumAndroidVersion") val minimumAndroidVersion: Int?, + @SerialName("latestAndroidVersion") val latestAndroidVersion: Int? +) \ No newline at end of file diff --git a/core/network/src/main/kotlin/com/team/bottles/network/dto/user/request/AlimyOnOffRequest.kt b/core/network/src/main/kotlin/com/team/bottles/network/dto/user/request/AlimyOnOffRequest.kt new file mode 100644 index 00000000..07fe8592 --- /dev/null +++ b/core/network/src/main/kotlin/com/team/bottles/network/dto/user/request/AlimyOnOffRequest.kt @@ -0,0 +1,11 @@ +package com.team.bottles.network.dto.user.request + +import com.team.bottles.network.dto.user.response.AlimyType +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class AlimyOnOffRequest( + @SerialName("alimyType") val alimyType: AlimyType, + @SerialName("enabled") val enabled: Boolean +) \ No newline at end of file diff --git a/core/network/src/main/kotlin/com/team/bottles/network/dto/user/response/AlimyResponse.kt b/core/network/src/main/kotlin/com/team/bottles/network/dto/user/response/AlimyResponse.kt new file mode 100644 index 00000000..301bb245 --- /dev/null +++ b/core/network/src/main/kotlin/com/team/bottles/network/dto/user/response/AlimyResponse.kt @@ -0,0 +1,19 @@ +package com.team.bottles.network.dto.user.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class AlimyResponse( + @SerialName("alimyType") val alimyType: AlimyType, + @SerialName("enabled") val enabled: Boolean +) + +@Serializable +enum class AlimyType { + @SerialName("DAILY_RANDOM") DAILY_RANDOM, + @SerialName("RECEIVE_LIKE") RECEIVE_LIKE, + @SerialName("PINGPONG") PING_PONG, + @SerialName("MARKETING") MARKETING, + ; +} \ No newline at end of file diff --git a/core/ui/src/main/kotlin/com/team/bottles/core/ui/Alert.kt b/core/ui/src/main/kotlin/com/team/bottles/core/ui/Alert.kt index a4a295de..d0a396d7 100644 --- a/core/ui/src/main/kotlin/com/team/bottles/core/ui/Alert.kt +++ b/core/ui/src/main/kotlin/com/team/bottles/core/ui/Alert.kt @@ -1,78 +1,292 @@ package com.team.bottles.core.ui -import androidx.compose.material3.AlertDialog +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +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.width +import androidx.compose.material3.BasicAlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon import androidx.compose.material3.Text 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.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.team.bottles.core.designsystem.R import com.team.bottles.core.designsystem.modifier.noRippleClickable import com.team.bottles.core.designsystem.theme.BottlesTheme @Composable -fun BottlesAlertDialog( +fun BottlesAlertConfirmDialog( modifier: Modifier = Modifier, onClose: () -> Unit, onConfirm: () -> Unit, - confirmText: String, - dismissText: String, + confirmButtonText: String, title: String, - content: String + content: String, ) { - AlertDialog( + BottlesAlertDialog( modifier = modifier, - onDismissRequest = onClose, - confirmButton = { + onClose = onClose, + title = title, + content = content + ) { + Box( + modifier = modifier + .fillMaxWidth() + .height(height = 36.dp) + .background( + color = BottlesTheme.color.container.enabledSecondary, + shape = BottlesTheme.shape.extraSmall + ) + .clip(shape = BottlesTheme.shape.extraSmall) + .noRippleClickable(onClick = onConfirm) + .padding(horizontal = 12.dp), + contentAlignment = Alignment.Center + ) { Text( - modifier = Modifier - .noRippleClickable( - onClick = onConfirm - ), - text = confirmText, - style = BottlesTheme.typography.kakaoLogin, - color = Color.Red + text = confirmButtonText, + style = BottlesTheme.typography.body, + color = BottlesTheme.color.text.enabledPrimary ) - }, - dismissButton = { - Text( - modifier = Modifier - .noRippleClickable( - onClick = onClose - ), - text = dismissText, - style = BottlesTheme.typography.kakaoLogin, - color = Color.Black + } + } +} + +@Composable +fun BottlesAlertDialogLeftConfirmRightDismiss( + modifier: Modifier = Modifier, + onClose: () -> Unit, + onDismiss: () -> Unit, + onConfirm: () -> Unit, + confirmButtonText: String, + dismissButtonText: String, + title: String, + content: String, +) { + BottlesAlertDialog( + modifier = modifier, + onClose = onClose, + title = title, + content = content + ) { + Row { + AlertDialogLeftButton( + onClick = onConfirm, + text = confirmButtonText, + ) + + Spacer(modifier = Modifier.width(width = BottlesTheme.spacing.small)) + + AlertDialogRightButton( + onClick = onDismiss, + text = dismissButtonText, + ) + } + } +} + +@Composable +fun BottlesAlertDialogLeftDismissRightConfirm( + modifier: Modifier = Modifier, + onClose: () -> Unit, + onDismiss: () -> Unit, + onConfirm: () -> Unit, + confirmButtonText: String, + dismissButtonText: String, + title: String, + content: String, +) { + BottlesAlertDialog( + modifier = modifier, + onClose = onClose, + title = title, + content = content + ) { + Row { + AlertDialogLeftButton( + onClick = onDismiss, + text = dismissButtonText, + ) + + Spacer(modifier = Modifier.width(width = BottlesTheme.spacing.small)) + + AlertDialogRightButton( + onClick = onConfirm, + text = confirmButtonText, ) - }, - title = { + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BottlesAlertDialog( + modifier: Modifier = Modifier, + onClose: () -> Unit, + title: String, + content: String, + buttons: @Composable () -> Unit +) { + BasicAlertDialog( + modifier = modifier, + onDismissRequest = onClose + ) { + Column( + modifier = Modifier + .background( + color = Color.White, + shape = BottlesTheme.shape.large + ) + .padding( + top = BottlesTheme.spacing.large, + bottom = BottlesTheme.spacing.medium, + start = BottlesTheme.spacing.medium, + end = BottlesTheme.spacing.medium, + ), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + painter = painterResource(id = R.drawable.ic_warning_24), + contentDescription = null, + tint = Color.Unspecified + ) + + Spacer(modifier = Modifier.height(height = BottlesTheme.spacing.extraSmall)) + Text( text = title, style = BottlesTheme.typography.subTitle1, - color = Color.Black + color = BottlesTheme.color.text.primary ) - }, - text = { + + Spacer(modifier = Modifier.height(height = BottlesTheme.spacing.doubleExtraSmall)) + Text( text = content, + textAlign = TextAlign.Center, style = BottlesTheme.typography.body, - color = Color.Gray + color = BottlesTheme.color.text.secondary ) + + Spacer( + modifier = Modifier.height( + height = BottlesTheme.spacing.small + BottlesTheme.spacing.medium + ) + ) + + buttons() } - ) + } +} +@Composable +private fun RowScope.AlertDialogRightButton( + modifier: Modifier = Modifier, + onClick: () -> Unit, + text: String, +) { + Box( + modifier = modifier + .height(height = 36.dp) + .weight(1f) + .background( + color = BottlesTheme.color.container.enabledSecondary, + shape = BottlesTheme.shape.extraSmall + ) + .clip(shape = BottlesTheme.shape.extraSmall) + .noRippleClickable(onClick = onClick) + .padding(horizontal = 12.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = text, + style = BottlesTheme.typography.body, + color = BottlesTheme.color.text.enabledPrimary + ) + } +} + +@Composable +private fun RowScope.AlertDialogLeftButton( + modifier: Modifier = Modifier, + onClick: () -> Unit, + text: String, +) { + Box( + modifier = modifier + .height(height = 36.dp) + .weight(1f) + .background( + color = BottlesTheme.color.container.disabledSecondary, + shape = BottlesTheme.shape.extraSmall + ) + .clip(shape = BottlesTheme.shape.extraSmall) + .noRippleClickable(onClick = onClick) + .padding(horizontal = 12.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = text, + style = BottlesTheme.typography.body, + color = BottlesTheme.color.text.enabledPrimary + ) + } +} + +@Preview(widthDp = 360, heightDp = 640, showBackground = true) +@Composable +private fun BottlesAlertDialogLeftDismissRightConfirmPreview() { + BottlesTheme { + BottlesAlertDialogLeftDismissRightConfirm( + onClose = {}, + onConfirm = {}, + onDismiss = {}, + confirmButtonText = "차단하기", + dismissButtonText = "취소하기", + title = "연락처 차단", + content = "주소록에 있는 000개의\n전화번호를 차단할까요?", + ) + } +} + +@Preview(widthDp = 360, heightDp = 640, showBackground = true) +@Composable +private fun BottlesAlertDialogLeftConfirmRightDismissPreview() { + BottlesTheme { + BottlesAlertDialogLeftConfirmRightDismiss( + onClose = {}, + onConfirm = {}, + onDismiss = {}, + confirmButtonText = "로그아웃하기", + dismissButtonText = "취소하기", + title = "로그아웃", + content = "정말 로그아웃하시겠어요?", + ) + } } -@Preview +@Preview(widthDp = 360, heightDp = 640, showBackground = true) @Composable -private fun BottlesAlterDialogPreview() { +private fun BottlesAlertConfirmDialogPreview() { BottlesTheme { - BottlesAlertDialog( + BottlesAlertConfirmDialog( onClose = {}, onConfirm = {}, - confirmText = "탈퇴하기", - dismissText = "취소하기", - title = "탈퇴하기", - content = "탈퇴 시 계정 복구가 어려워요.\n정말 탈퇴하시겠어요?" + confirmButtonText = "업데이트 하기", + title = "업데이트 안내", + content = "최적의 사용 환경을 위해\n최신 버전의 앱으로 업데이트 해주세요", ) } } \ No newline at end of file diff --git a/core/ui/src/main/kotlin/com/team/bottles/core/ui/CardProfile.kt b/core/ui/src/main/kotlin/com/team/bottles/core/ui/CardProfile.kt index 3ae96cfd..5415d53e 100644 --- a/core/ui/src/main/kotlin/com/team/bottles/core/ui/CardProfile.kt +++ b/core/ui/src/main/kotlin/com/team/bottles/core/ui/CardProfile.kt @@ -2,45 +2,29 @@ package com.team.bottles.core.ui -import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.team.bottles.core.designsystem.components.buttons.BottlesOutLinedButton import com.team.bottles.core.designsystem.components.buttons.OutlinedButtonType +import com.team.bottles.core.designsystem.components.cards.BottlesCard import com.team.bottles.core.designsystem.theme.BottlesTheme import com.team.bottles.core.ui.model.UserKeyPoint @Composable fun CardProfile( + modifier: Modifier = Modifier, keyPoints: List, ) { - Column( - modifier = Modifier - .background( - color = BottlesTheme.color.container.primary, - shape = BottlesTheme.shape.extraLarge - ) - .clip(shape = BottlesTheme.shape.extraLarge) - .border( - width = 1.dp, - shape = BottlesTheme.shape.extraLarge, - color = BottlesTheme.color.border.primary - ) - .padding( - vertical = BottlesTheme.spacing.extraLarge, - horizontal = BottlesTheme.spacing.small - ), + BottlesCard( + modifier = modifier, verticalArrangement = Arrangement.spacedBy( space = BottlesTheme.spacing.extraLarge ) diff --git a/core/ui/src/main/kotlin/com/team/bottles/core/ui/model/AlertType.kt b/core/ui/src/main/kotlin/com/team/bottles/core/ui/model/AlertType.kt index 641e93fa..68069dd8 100644 --- a/core/ui/src/main/kotlin/com/team/bottles/core/ui/model/AlertType.kt +++ b/core/ui/src/main/kotlin/com/team/bottles/core/ui/model/AlertType.kt @@ -17,24 +17,34 @@ enum class AlertType( override val dismissText: String, override val title: String, override val content: String -): AlertInfo { +) : AlertInfo { LOG_OUT( title = "로그아웃", content = "정말 로그아웃 하시겠어요?", - confirmText = "로그아웃", + confirmText = "로그아웃하기", dismissText = "취소하기" ), DELETE_USER( title = "탈퇴하기", - content = "탈퇴 시 계정 복구가 어려워요.\n정말 탈퇴하시겠어요?", + content = "탈퇴 시 계정 복구가 어려워요.\n" + + "정말 탈퇴하시겠어요?", confirmText = "탈퇴하기", dismissText = "취소하기" ), STOP_PING_PONG( title = "중단하기", - content = "중단 시 모든 핑퐁 내용이 사라져요.\n정말 중단하시겠어요?", + content = "중단 시 모든 핑퐁 내용이 사라져요.\n" + + "정말 중단하시겠어요?", confirmText = "중단하기", dismissText = "계속하기" ), + USER_REPORT( + title = "신고하기", + content = "접수 후 취소할 수 없으며\n" + + "해당 사용자는 차단돼요.\n" + + "정말 신고하시겠어요?", + confirmText = "신고하기", + dismissText = "계속하기" + ) ; } diff --git a/feat/mypage/build.gradle.kts b/feat/mypage/build.gradle.kts index f117b579..961f5472 100644 --- a/feat/mypage/build.gradle.kts +++ b/feat/mypage/build.gradle.kts @@ -1,5 +1,3 @@ -import org.jetbrains.kotlin.konan.properties.Properties - plugins { id("team.bottles.android.library.compose") id("team.bottles.android.library") @@ -11,24 +9,9 @@ plugins { android { namespace = "com.team.bottles.feat.mypage" - buildTypes { - val debugUrl = "BOTTLES_MY_PAGE_URL" - val properties = Properties().apply { load(rootProject.file("local.properties").inputStream()) } - - getByName("release") { - buildConfigField( - "String", - debugUrl, // TODO : 릴리즈 용 URL 생성 가능성 있음 - properties.getProperty(debugUrl) // // TODO : 릴리즈 용 URL 생성 가능성 있음 - ) - } - getByName("debug") { - buildConfigField( - "String", - debugUrl, - properties.getProperty(debugUrl) - ) - } + defaultConfig { + buildConfigField("String", "VERSION_NAME", "\"${libs.versions.versionName.get()}\"") + buildConfigField("Integer", "VERSION_CODE", libs.versions.versionCode.get()) } } diff --git a/feat/mypage/src/main/kotlin/com/team/bottles/feat/mypage/MyPageBridge.kt b/feat/mypage/src/main/kotlin/com/team/bottles/feat/mypage/MyPageBridge.kt deleted file mode 100644 index 9b557d07..00000000 --- a/feat/mypage/src/main/kotlin/com/team/bottles/feat/mypage/MyPageBridge.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.team.bottles.feat.mypage - -import android.webkit.JavascriptInterface - -internal class MyPageBridge(private val onAction: (MyPageWebAction) -> Unit) : MyPageBridgeListener { - - @JavascriptInterface - override fun logout() { - onAction(MyPageWebAction.OnLogOut) - } - - @JavascriptInterface - override fun deleteUser() { - onAction(MyPageWebAction.OnDeleteUser) - } - - companion object { - const val NAME = "Native" - } - -} - -sealed interface MyPageWebAction { - - data object OnLogOut : MyPageWebAction - - data object OnDeleteUser : MyPageWebAction - -} - -interface MyPageBridgeListener { - - fun logout() - - fun deleteUser() - -} diff --git a/feat/mypage/src/main/kotlin/com/team/bottles/feat/mypage/MyPageRoute.kt b/feat/mypage/src/main/kotlin/com/team/bottles/feat/mypage/MyPageRoute.kt index a1b42c25..c53b3dc3 100644 --- a/feat/mypage/src/main/kotlin/com/team/bottles/feat/mypage/MyPageRoute.kt +++ b/feat/mypage/src/main/kotlin/com/team/bottles/feat/mypage/MyPageRoute.kt @@ -1,8 +1,19 @@ package com.team.bottles.feat.mypage +import android.Manifest +import android.content.ActivityNotFoundException +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.provider.Settings +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.LocalContext +import androidx.core.content.ContextCompat import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.team.bottles.feat.mypage.mvi.MyPageSideEffect @@ -10,14 +21,75 @@ import com.team.bottles.feat.mypage.mvi.MyPageSideEffect @Composable internal fun MyPageRoute( viewModel: MyPageViewModel = hiltViewModel(), - navigateToLoginEndPoint: () -> Unit + navigateToEditProfile: () -> Unit, + navigateToSettingNotification: () -> Unit, + navigateToSettingAccountManagement: () -> Unit, ) { val uiState by viewModel.state.collectAsStateWithLifecycle() + val context = LocalContext.current + val permissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + onResult = { isGranted -> + if (isGranted) { + viewModel.fetchContacts() + } else { + Toast.makeText(context,"연락처 권한을 동의 해야합니다.", Toast.LENGTH_SHORT).show() + + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", context.packageName, null) + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + + context.startActivity(intent) + } + } + ) + + LaunchedEffect(Unit) { + viewModel.checkAppVersion() + } LaunchedEffect(Unit) { viewModel.sideEffect.collect { sideEffect -> when (sideEffect) { - is MyPageSideEffect.NavigateToLoginEndPoint -> navigateToLoginEndPoint() + is MyPageSideEffect.NavigateToEditProfile -> navigateToEditProfile() + is MyPageSideEffect.NavigateToSettingNotification -> navigateToSettingNotification() + is MyPageSideEffect.NavigateToSettingAccountManagement -> navigateToSettingAccountManagement() + is MyPageSideEffect.NavigateToPolicyNotion -> { + val intent = + Intent(Intent.ACTION_VIEW, Uri.parse("https://spiral-ogre-a4d.notion.site/abb2fd284516408e8c2fc267d07c6421")) // 개인 정보 처리 방침 URL + context.startActivity(intent) + } + is MyPageSideEffect.NavigateToTermsOfUseNotion -> { + val intent = + Intent(Intent.ACTION_VIEW, Uri.parse("https://spiral-ogre-a4d.notion.site/240724-e3676639ea864147bb293cfcda40d99f")) // 이용약관 URL + context.startActivity(intent) + } + is MyPageSideEffect.NavigateToKakaoBusinessChannel -> { + try { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse("kakaoplus://plusfriend/friend/_hDIQG")) // 카톡으로 카카오 플러스 열기 + context.startActivity(intent) + } catch (e: ActivityNotFoundException) { + val webIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://pf.kakao.com/_hDIQG")) // URL로 카카오 플러스 열기 + context.startActivity(webIntent) + } + } + is MyPageSideEffect.CheckContactPermission -> { + if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { + permissionLauncher.launch(Manifest.permission.READ_CONTACTS) + } else { + viewModel.fetchContacts() + } + } + is MyPageSideEffect.NavigateToPlayStore -> { + try { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=com.team.bottles&hl=ko")) + context.startActivity(intent) + } catch (e: ActivityNotFoundException) { + val webIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=com.team.bottles&hl=ko")) + context.startActivity(webIntent) + } + } } } } diff --git a/feat/mypage/src/main/kotlin/com/team/bottles/feat/mypage/MyPageScreen.kt b/feat/mypage/src/main/kotlin/com/team/bottles/feat/mypage/MyPageScreen.kt index 6033acdc..5b4fc9ad 100644 --- a/feat/mypage/src/main/kotlin/com/team/bottles/feat/mypage/MyPageScreen.kt +++ b/feat/mypage/src/main/kotlin/com/team/bottles/feat/mypage/MyPageScreen.kt @@ -1,68 +1,79 @@ package com.team.bottles.feat.mypage -import android.annotation.SuppressLint -import android.webkit.WebView -import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.team.bottles.core.designsystem.components.bars.BottlesTopBar +import com.team.bottles.core.designsystem.components.etc.BottlesUserInfo import com.team.bottles.core.designsystem.theme.BottlesTheme -import com.team.bottles.core.ui.BottlesAlertDialog -import com.team.bottles.core.ui.BottlesWebView -import com.team.bottles.core.ui.model.AlertType +import com.team.bottles.core.ui.BottlesAlertDialogLeftDismissRightConfirm +import com.team.bottles.feat.mypage.components.SettingList import com.team.bottles.feat.mypage.mvi.MyPageIntent import com.team.bottles.feat.mypage.mvi.MyPageUiState -@SuppressLint("JavascriptInterface") @Composable internal fun MyPageScreen( uiState: MyPageUiState, onIntent: (MyPageIntent) -> Unit ) { - val context = LocalContext.current - val webView = remember { - WebView(context).apply { - addJavascriptInterface( - MyPageBridge { webAction -> - when (webAction) { - is MyPageWebAction.OnDeleteUser -> onIntent(MyPageIntent.ClickWebDeleteUserButton) - is MyPageWebAction.OnLogOut -> onIntent(MyPageIntent.ClickWebLogOutButton) - } - }, - MyPageBridge.NAME - ) - } + val scrollState = rememberScrollState() + + if (uiState.showDialog) { + BottlesAlertDialogLeftDismissRightConfirm( + onClose = { onIntent(MyPageIntent.CloseDialog) }, + onDismiss = { onIntent(MyPageIntent.CloseDialog) }, + onConfirm = { onIntent(MyPageIntent.ClickConfirmButton) }, + confirmButtonText = "차단하기", + dismissButtonText = "취소하기", + title = "연락처 차단", + content = "주소록에 있는 ${uiState.inDeviceContacts.size}개의\n" + + "전화번호를 차단할까요?" + ) } - Box( - modifier = Modifier.fillMaxSize() + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(state = scrollState) ) { - if (uiState.showDialog) { - BottlesAlertDialog( - onClose = { onIntent(MyPageIntent.ClickCancel) }, - onConfirm = { - if (uiState.alertType == AlertType.LOG_OUT) { - onIntent(MyPageIntent.ClickDialogLogOutButton) - } else if (uiState.alertType == AlertType.DELETE_USER) { - onIntent(MyPageIntent.ClickDialogDeleteUserButton) - } - }, - confirmText = uiState.alertType.confirmText, - dismissText = uiState.alertType.dismissText, - title = uiState.alertType.title, - content = uiState.alertType.content - ) - } + BottlesTopBar() + + Spacer(modifier = Modifier.height(height = BottlesTheme.spacing.doubleExtraLarge)) + + BottlesUserInfo( + modifier = Modifier.padding(horizontal = 32.dp), + imageUrl = uiState.imageUrl, + userName = uiState.userName, + userAge = uiState.userAge, + isBlur = false + ) + + Spacer(modifier = Modifier.height(height = BottlesTheme.spacing.doubleExtraLarge)) + + SettingList( + modifier = Modifier.padding(horizontal = 16.dp), + onClickEditProfile = { onIntent(MyPageIntent.ClickEditProfile) }, + onClickUpdateBlockContact = { onIntent(MyPageIntent.ClickUpdateBlockContact) }, + onClickSettingNotification = { onIntent(MyPageIntent.ClickSettingNotification) }, + onClickAccountManagement = { onIntent(MyPageIntent.ClickAccountManagement) }, + onClickUpdateAppVersion = { onIntent(MyPageIntent.ClickUpdateAppVersion) }, + onClickAsk = { onIntent(MyPageIntent.ClickAsk) }, + onClickTermsOfUse = { onIntent(MyPageIntent.ClickTermsOfUse) }, + onClickPolicy = { onIntent(MyPageIntent.ClickPolicy) }, + blockedUserValue = uiState.blockedUserValue, + appVersion = uiState.appVersionName, + canUpdateAppVersion = uiState.canUpdateAppVersion + ) - if (uiState.token.accessToken.isNotEmpty() && uiState.token.refreshToken.isNotEmpty()) { - BottlesWebView( - url = BuildConfig.BOTTLES_MY_PAGE_URL + "?accessToken=${uiState.token.accessToken}&refreshToken=${uiState.token.refreshToken}", - webView = webView - ) - } + Spacer(modifier = Modifier.height(height = 24.dp)) } } @@ -71,7 +82,11 @@ internal fun MyPageScreen( private fun MyPageScreenPreview() { BottlesTheme { MyPageScreen( - uiState = MyPageUiState(showDialog = true), + uiState = MyPageUiState( + userName = "뇽뇽이", + userAge = 15, + appVersionName = "1.0.0" + ), onIntent = {} ) } diff --git a/feat/mypage/src/main/kotlin/com/team/bottles/feat/mypage/MyPageViewModel.kt b/feat/mypage/src/main/kotlin/com/team/bottles/feat/mypage/MyPageViewModel.kt index 6f116b44..08dd4a17 100644 --- a/feat/mypage/src/main/kotlin/com/team/bottles/feat/mypage/MyPageViewModel.kt +++ b/feat/mypage/src/main/kotlin/com/team/bottles/feat/mypage/MyPageViewModel.kt @@ -2,11 +2,10 @@ package com.team.bottles.feat.mypage import androidx.lifecycle.SavedStateHandle import com.team.bottles.core.common.BaseViewModel -import com.team.bottles.core.domain.auth.model.Token -import com.team.bottles.core.domain.auth.usecase.DeleteUserUseCase -import com.team.bottles.core.domain.auth.usecase.LogOutUseCase -import com.team.bottles.core.domain.auth.usecase.WebViewConnectUseCase -import com.team.bottles.core.ui.model.AlertType +import com.team.bottles.core.domain.auth.usecase.GetLatestAppVersionUseCase +import com.team.bottles.core.domain.profile.usecase.GetUserProfileUseCase +import com.team.bottles.core.domain.user.usecase.GetContactsUseCase +import com.team.bottles.core.domain.user.usecase.UpdateBlockingContactsUseCase import com.team.bottles.feat.mypage.mvi.MyPageIntent import com.team.bottles.feat.mypage.mvi.MyPageSideEffect import com.team.bottles.feat.mypage.mvi.MyPageUiState @@ -15,16 +14,23 @@ import javax.inject.Inject @HiltViewModel class MyPageViewModel @Inject constructor( - private val logOutUseCase: LogOutUseCase, - private val deleteUserUseCase: DeleteUserUseCase, - private val webViewConnectUseCase: WebViewConnectUseCase, + private val getContactsUseCase: GetContactsUseCase, + private val getLatestAppVersionUseCase: GetLatestAppVersionUseCase, + private val updateBlockingContactsUseCase: UpdateBlockingContactsUseCase, + private val getUserProfileUseCase: GetUserProfileUseCase, savedStateHandle: SavedStateHandle ) : BaseViewModel( savedStateHandle ) { init { - initialWebConnect() + launch { + val profile = getUserProfileUseCase() + val userImageUrl = profile.imageUrl + val userName = profile.userName + val userAge = profile.age + reduce { copy(imageUrl = userImageUrl, userName = userName, userAge = userAge) } + } } override fun createInitialState(savedStateHandle: SavedStateHandle): MyPageUiState = @@ -32,11 +38,16 @@ class MyPageViewModel @Inject constructor( override suspend fun handleIntent(intent: MyPageIntent) { when (intent) { - is MyPageIntent.ClickWebLogOutButton -> reduce { copy(alertType = AlertType.LOG_OUT, showDialog = true) } - is MyPageIntent.ClickWebDeleteUserButton -> reduce { copy(alertType = AlertType.DELETE_USER, showDialog = true) } - is MyPageIntent.ClickCancel -> reduce { copy(showDialog = false) } - is MyPageIntent.ClickDialogLogOutButton -> logOut() - is MyPageIntent.ClickDialogDeleteUserButton -> deleteUser() + is MyPageIntent.ClickEditProfile -> navigateToEditProfile() + is MyPageIntent.ClickUpdateBlockContact -> checkContactPermission() + is MyPageIntent.ClickSettingNotification -> navigateToSettingNotification() + is MyPageIntent.ClickAccountManagement -> navigateToSettingAccountManagement() + is MyPageIntent.ClickUpdateAppVersion -> navigateToPlayStore() + is MyPageIntent.ClickAsk -> navigateToKakaoBusinessChannel() + is MyPageIntent.ClickTermsOfUse -> navigateToTermsOfUseNotion() + is MyPageIntent.ClickPolicy -> navigateToPolicyNotion() + is MyPageIntent.ClickConfirmButton -> updateBlockContact() + is MyPageIntent.CloseDialog -> closeDialog() } } @@ -44,35 +55,71 @@ class MyPageViewModel @Inject constructor( TODO("Not yet implemented") } - private fun logOut() { + private fun navigateToEditProfile() { + postSideEffect(MyPageSideEffect.NavigateToEditProfile) + } + + private fun navigateToSettingNotification() { + postSideEffect(MyPageSideEffect.NavigateToSettingNotification) + } + + private fun navigateToSettingAccountManagement() { + postSideEffect(MyPageSideEffect.NavigateToSettingAccountManagement) + } + + private fun navigateToKakaoBusinessChannel() { + postSideEffect(MyPageSideEffect.NavigateToKakaoBusinessChannel) + } + + private fun navigateToTermsOfUseNotion() { + postSideEffect(MyPageSideEffect.NavigateToTermsOfUseNotion) + } + + private fun navigateToPolicyNotion() { + postSideEffect(MyPageSideEffect.NavigateToPolicyNotion) + } + + private fun navigateToPlayStore() { + postSideEffect(MyPageSideEffect.NavigateToPlayStore) + } + + private fun checkContactPermission() { + postSideEffect(MyPageSideEffect.CheckContactPermission) + } + + private fun showBlockContactDialog() { + reduce { copy(showDialog = true) } + } + + private fun closeDialog() { + reduce { copy(showDialog = false) } + } + + private fun updateBlockContact() { launch { - logOutUseCase() + updateBlockingContactsUseCase(contacts = currentState.inDeviceContacts) + // TODO : 차단한 연락처 갯수 얻는 API 호출 reduce { copy(showDialog = false) } - postSideEffect(MyPageSideEffect.NavigateToLoginEndPoint) } } - private fun deleteUser() { + fun checkAppVersion() { launch { - deleteUserUseCase() - reduce { copy(showDialog = false) } - postSideEffect(MyPageSideEffect.NavigateToLoginEndPoint) + val latestAppVersionCode = getLatestAppVersionUseCase() + val currentAppVersion = currentState.appVersionCode + + if (latestAppVersionCode > currentAppVersion) { + reduce { copy(canUpdateAppVersion = true) } + } } } - private fun initialWebConnect() { + fun fetchContacts() { launch { - webViewConnectUseCase.getLocalToken().run { - reduce { - copy( - token = Token( - accessToken = accessToken, - refreshToken = refreshToken - ) - ) - } - } + val contacts = getContactsUseCase() + reduce { copy(inDeviceContacts = contacts) } + showBlockContactDialog() } } -} \ No newline at end of file +} diff --git a/feat/mypage/src/main/kotlin/com/team/bottles/feat/mypage/components/SettingList.kt b/feat/mypage/src/main/kotlin/com/team/bottles/feat/mypage/components/SettingList.kt new file mode 100644 index 00000000..ebaf950d --- /dev/null +++ b/feat/mypage/src/main/kotlin/com/team/bottles/feat/mypage/components/SettingList.kt @@ -0,0 +1,129 @@ +package com.team.bottles.feat.mypage.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.HorizontalDivider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.team.bottles.core.designsystem.components.cards.BottlesCard +import com.team.bottles.core.designsystem.components.lists.BottlesSettingItem +import com.team.bottles.core.designsystem.components.lists.BottlesSettingItemWithArrow +import com.team.bottles.core.designsystem.components.lists.BottlesSettingItemWithButton +import com.team.bottles.core.designsystem.theme.BottlesTheme + +@Composable +internal fun SettingList( + modifier: Modifier = Modifier, + onClickEditProfile: () -> Unit, + onClickUpdateBlockContact: () -> Unit, + onClickSettingNotification: () -> Unit, + onClickAccountManagement: () -> Unit, + onClickUpdateAppVersion: () -> Unit, + onClickAsk: () -> Unit, + onClickTermsOfUse: () -> Unit, + onClickPolicy: () -> Unit, + blockedUserValue: Int, + appVersion: String, + canUpdateAppVersion: Boolean, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy( + space = BottlesTheme.spacing.small + ) + ) { + /*BottlesCard { + BottlesSettingItemWithArrow( + title = "프로필 수정", + onClickItem = onClickEditProfile + ) + }*/ // TODO : 웹뷰 작업 완료시 기능 추가 + + BottlesCard( + verticalArrangement = Arrangement.spacedBy( + space = BottlesTheme.spacing.large + ) + ) { + BottlesSettingItemWithButton( + title = "연락처 차단", + subTitle = "기기 내 모든 연락처 차단", // TODO : 연락처 속 ${blockedUserValue}명을 차단햇어요 + onClickButton = onClickUpdateBlockContact, + buttonText = "업데이트" + ) + + BottlesSettingItemWithArrow( + title = "알림 설정", + onClickItem = onClickSettingNotification + ) + + BottlesSettingItemWithArrow( + title = "계정 관리", + onClickItem = onClickAccountManagement + ) + + HorizontalDivider( + modifier = Modifier.fillMaxWidth(), + thickness = 1.dp, + color = BottlesTheme.color.border.secondary + ) + + if (canUpdateAppVersion) { + BottlesSettingItemWithButton( + title = "앱 버전", + subTitle = appVersion, + onClickButton = onClickUpdateAppVersion, + buttonText = "업데이트" + ) + } else { + BottlesSettingItem( + title = "앱 버전", + subTitle = appVersion + ) + } + + BottlesSettingItemWithArrow( + title = "1:1 문의", + onClickItem = onClickAsk + ) + + HorizontalDivider( + modifier = Modifier.fillMaxWidth(), + thickness = 1.dp, + color = BottlesTheme.color.border.secondary + ) + + BottlesSettingItemWithArrow( + title = "보틀 이용약관", + onClickItem = onClickTermsOfUse + ) + + BottlesSettingItemWithArrow( + title = "개인정보처리방침", + onClickItem = onClickPolicy + ) + } + } +} + +@Preview +@Composable +private fun SettingListPreview() { + BottlesTheme { + SettingList( + onClickEditProfile = { /*TODO*/ }, + onClickUpdateBlockContact = { /*TODO*/ }, + onClickSettingNotification = { /*TODO*/ }, + onClickAccountManagement = { /*TODO*/ }, + onClickUpdateAppVersion = { /*TODO*/ }, + onClickAsk = { /*TODO*/ }, + onClickTermsOfUse = { /*TODO*/ }, + onClickPolicy = { /*TODO*/ }, + blockedUserValue = 5, + appVersion = "1.0.0", + canUpdateAppVersion = false + ) + } +} \ No newline at end of file diff --git a/feat/mypage/src/main/kotlin/com/team/bottles/feat/mypage/mvi/MyPageIntent.kt b/feat/mypage/src/main/kotlin/com/team/bottles/feat/mypage/mvi/MyPageIntent.kt index 128c6b6b..a2950f4a 100644 --- a/feat/mypage/src/main/kotlin/com/team/bottles/feat/mypage/mvi/MyPageIntent.kt +++ b/feat/mypage/src/main/kotlin/com/team/bottles/feat/mypage/mvi/MyPageIntent.kt @@ -4,14 +4,24 @@ import com.team.bottles.core.common.UiIntent sealed interface MyPageIntent : UiIntent { - data object ClickWebLogOutButton : MyPageIntent + data object ClickEditProfile : MyPageIntent - data object ClickWebDeleteUserButton : MyPageIntent + data object ClickUpdateBlockContact : MyPageIntent - data object ClickDialogDeleteUserButton : MyPageIntent + data object ClickSettingNotification : MyPageIntent - data object ClickDialogLogOutButton : MyPageIntent + data object ClickAccountManagement : MyPageIntent - data object ClickCancel : MyPageIntent + data object ClickUpdateAppVersion : MyPageIntent + + data object ClickAsk : MyPageIntent + + data object ClickTermsOfUse : MyPageIntent + + data object ClickPolicy : MyPageIntent + + data object ClickConfirmButton : MyPageIntent + + data object CloseDialog : MyPageIntent } \ No newline at end of file diff --git a/feat/mypage/src/main/kotlin/com/team/bottles/feat/mypage/mvi/MyPageSideEffect.kt b/feat/mypage/src/main/kotlin/com/team/bottles/feat/mypage/mvi/MyPageSideEffect.kt index ae6dbf73..aa7b9e44 100644 --- a/feat/mypage/src/main/kotlin/com/team/bottles/feat/mypage/mvi/MyPageSideEffect.kt +++ b/feat/mypage/src/main/kotlin/com/team/bottles/feat/mypage/mvi/MyPageSideEffect.kt @@ -4,6 +4,20 @@ import com.team.bottles.core.common.UiSideEffect sealed interface MyPageSideEffect : UiSideEffect { - data object NavigateToLoginEndPoint : MyPageSideEffect + data object NavigateToEditProfile : MyPageSideEffect + + data object NavigateToSettingNotification : MyPageSideEffect + + data object NavigateToSettingAccountManagement : MyPageSideEffect + + data object NavigateToKakaoBusinessChannel : MyPageSideEffect + + data object NavigateToTermsOfUseNotion : MyPageSideEffect + + data object NavigateToPolicyNotion : MyPageSideEffect + + data object CheckContactPermission : MyPageSideEffect + + data object NavigateToPlayStore : MyPageSideEffect } \ No newline at end of file diff --git a/feat/mypage/src/main/kotlin/com/team/bottles/feat/mypage/mvi/MyPageUiState.kt b/feat/mypage/src/main/kotlin/com/team/bottles/feat/mypage/mvi/MyPageUiState.kt index 91124d72..6a0d4f6a 100644 --- a/feat/mypage/src/main/kotlin/com/team/bottles/feat/mypage/mvi/MyPageUiState.kt +++ b/feat/mypage/src/main/kotlin/com/team/bottles/feat/mypage/mvi/MyPageUiState.kt @@ -1,11 +1,16 @@ package com.team.bottles.feat.mypage.mvi import com.team.bottles.core.common.UiState -import com.team.bottles.core.domain.auth.model.Token -import com.team.bottles.core.ui.model.AlertType +import com.team.bottles.feat.mypage.BuildConfig data class MyPageUiState( - val token: Token = Token(), - val alertType: AlertType = AlertType.LOG_OUT, val showDialog: Boolean = false, + val imageUrl: String = "", + val userName: String = "", + val userAge: Int = 0, + val blockedUserValue: Int = 0, + val appVersionName: String = BuildConfig.VERSION_NAME, + val appVersionCode: Int = BuildConfig.VERSION_CODE, + val canUpdateAppVersion: Boolean = false, + val inDeviceContacts: List = emptyList(), ) : UiState diff --git a/feat/mypage/src/main/kotlin/com/team/bottles/feat/mypage/navigation/MyPageNavigation.kt b/feat/mypage/src/main/kotlin/com/team/bottles/feat/mypage/navigation/MyPageNavigation.kt index 8b453c01..2d44222e 100644 --- a/feat/mypage/src/main/kotlin/com/team/bottles/feat/mypage/navigation/MyPageNavigation.kt +++ b/feat/mypage/src/main/kotlin/com/team/bottles/feat/mypage/navigation/MyPageNavigation.kt @@ -6,9 +6,15 @@ import androidx.navigation.compose.composable import com.team.bottles.feat.mypage.MyPageRoute fun NavGraphBuilder.myPageScreen( - navigateToLoginEndPoint: () -> Unit + navigateToEditProfile: () -> Unit, + navigateToSettingNotification: () -> Unit, + navigateToSettingAccountManagement: () -> Unit, ) { composable { - MyPageRoute(navigateToLoginEndPoint = navigateToLoginEndPoint) + MyPageRoute( + navigateToEditProfile = navigateToEditProfile, + navigateToSettingNotification = navigateToSettingNotification, + navigateToSettingAccountManagement = navigateToSettingAccountManagement + ) } } diff --git a/feat/onboarding/src/main/kotlin/com/team/bottles/feat/onboarding/OnboardingScreen.kt b/feat/onboarding/src/main/kotlin/com/team/bottles/feat/onboarding/OnboardingScreen.kt index 31df87a2..77c603ab 100644 --- a/feat/onboarding/src/main/kotlin/com/team/bottles/feat/onboarding/OnboardingScreen.kt +++ b/feat/onboarding/src/main/kotlin/com/team/bottles/feat/onboarding/OnboardingScreen.kt @@ -11,8 +11,14 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +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.Color +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import com.team.bottles.core.designsystem.R @@ -34,8 +40,10 @@ internal fun OnboardingScreen( uiState: OnboardingUiState, onIntent: (OnboardingIntent) -> Unit ) { + var bottomBarHeight by remember { mutableIntStateOf(0) } + Scaffold( - containerColor = BottlesTheme.color.background.primary, + containerColor = Color.Transparent, modifier = Modifier.fillMaxSize(), topBar = { BottlesTopBar( @@ -52,8 +60,27 @@ internal fun OnboardingScreen( } ) }, + bottomBar = { + BottlesBottomBar( + modifier = Modifier + .onGloballyPositioned { + bottomBarHeight = it.size.height + }, + text = if (uiState.currentPage.ordinal + 2 != uiState.maxPage) "다음" + else "확인", + onClick = { onIntent(OnboardingIntent.ClickNextButton) }, + enabled = true, + isDebounce = false + ) + } ) { contentPadding -> - Box(modifier = Modifier.padding(contentPadding)) { + Box( + modifier = Modifier + .background(color = BottlesTheme.color.background.primary) + .padding( + top = contentPadding.calculateTopPadding(), + ) + ) { Column( modifier = Modifier .fillMaxSize() @@ -63,7 +90,7 @@ internal fun OnboardingScreen( Spacer(modifier = Modifier.height(height = BottlesTheme.spacing.extraLarge)) StepTitle( - currentPage = uiState.currentPage.ordinal + 2, + currentPage = uiState.currentPage.ordinal + 1, maxPage = uiState.maxPage, titleText = uiState.currentPage.title ) @@ -77,16 +104,8 @@ internal fun OnboardingScreen( OnboardingPage.FOUR -> StepFour() } - Spacer(modifier = Modifier.height(height = BottlesTheme.spacing.extraLarge)) + Spacer(modifier = Modifier.height(height = (bottomBarHeight / 2).dp)) } - - BottlesBottomBar( - modifier = Modifier.align(Alignment.BottomCenter), - text = if (uiState.currentPage.ordinal + 2 != uiState.maxPage) "다음" - else "확인", - onClick = { onIntent(OnboardingIntent.ClickNextButton) }, - enabled = true - ) } } } diff --git a/feat/onboarding/src/main/kotlin/com/team/bottles/feat/onboarding/OnboardingViewModel.kt b/feat/onboarding/src/main/kotlin/com/team/bottles/feat/onboarding/OnboardingViewModel.kt index 06eed36d..f1954964 100644 --- a/feat/onboarding/src/main/kotlin/com/team/bottles/feat/onboarding/OnboardingViewModel.kt +++ b/feat/onboarding/src/main/kotlin/com/team/bottles/feat/onboarding/OnboardingViewModel.kt @@ -30,7 +30,7 @@ class OnboardingViewModel @Inject constructor( } private fun nextPage() { - if (currentState.currentPage.ordinal + 3 > currentState.maxPage) { + if (currentState.currentPage.ordinal + 2 > currentState.maxPage) { navigateToCreateProfile() } else { reduce { copy(currentPage = OnboardingPage.entries[currentPage.ordinal + 1]) } diff --git a/feat/onboarding/src/main/kotlin/com/team/bottles/feat/onboarding/mvi/OnboardingUiState.kt b/feat/onboarding/src/main/kotlin/com/team/bottles/feat/onboarding/mvi/OnboardingUiState.kt index 0106d7f3..62a72e05 100644 --- a/feat/onboarding/src/main/kotlin/com/team/bottles/feat/onboarding/mvi/OnboardingUiState.kt +++ b/feat/onboarding/src/main/kotlin/com/team/bottles/feat/onboarding/mvi/OnboardingUiState.kt @@ -4,7 +4,7 @@ import com.team.bottles.core.common.UiState data class OnboardingUiState( val currentPage: OnboardingPage = OnboardingPage.ONE, - val maxPage: Int = OnboardingPage.entries.size + 1, + val maxPage: Int = OnboardingPage.entries.size, ): UiState enum class OnboardingPage(val title: String) { diff --git a/feat/ping-pong/src/main/kotlin/com/team/bottles/feat/pingpong/PingPongScreen.kt b/feat/ping-pong/src/main/kotlin/com/team/bottles/feat/pingpong/PingPongScreen.kt index e8ceaf6a..132e85df 100644 --- a/feat/ping-pong/src/main/kotlin/com/team/bottles/feat/pingpong/PingPongScreen.kt +++ b/feat/ping-pong/src/main/kotlin/com/team/bottles/feat/pingpong/PingPongScreen.kt @@ -31,7 +31,7 @@ import com.team.bottles.core.designsystem.theme.BottlesTheme import com.team.bottles.core.domain.bottle.model.PingPongLetter import com.team.bottles.core.domain.bottle.model.PingPongMatchStatus import com.team.bottles.core.domain.profile.model.UserProfile -import com.team.bottles.core.ui.BottlesAlertDialog +import com.team.bottles.core.ui.BottlesAlertDialogLeftDismissRightConfirm import com.team.bottles.core.ui.model.AlertType import com.team.bottles.feat.pingpong.components.PingPongBottomBar import com.team.bottles.feat.pingpong.components.PingPongTopBar @@ -75,11 +75,12 @@ internal fun PingPongScreen( } if (uiState.showDialog) { - BottlesAlertDialog( + BottlesAlertDialogLeftDismissRightConfirm( onClose = { onIntent(PingPongIntent.ClickCloseAlert) }, onConfirm = { onIntent(PingPongIntent.ClickConfirmAlert) }, - confirmText = AlertType.STOP_PING_PONG.confirmText, - dismissText = AlertType.STOP_PING_PONG.dismissText, + onDismiss = { onIntent(PingPongIntent.ClickCloseAlert) }, + confirmButtonText = AlertType.STOP_PING_PONG.confirmText, + dismissButtonText = AlertType.STOP_PING_PONG.dismissText, title = AlertType.STOP_PING_PONG.title, content = AlertType.STOP_PING_PONG.content ) @@ -233,6 +234,7 @@ private fun PingPongScreenPreview() { BottlesTheme { PingPongScreen( uiState = PingPongUiState( + showDialog = true, currentTab = PingPongTab.INTRODUCTION, pingPongMatchStatus = PingPongMatchStatus.NONE, partnerProfile = UserProfile.sampleUserProfile(), diff --git a/feat/ping-pong/src/main/kotlin/com/team/bottles/feat/pingpong/components/IntroductionContents.kt b/feat/ping-pong/src/main/kotlin/com/team/bottles/feat/pingpong/components/IntroductionContents.kt index bbc5d750..5f241914 100644 --- a/feat/ping-pong/src/main/kotlin/com/team/bottles/feat/pingpong/components/IntroductionContents.kt +++ b/feat/ping-pong/src/main/kotlin/com/team/bottles/feat/pingpong/components/IntroductionContents.kt @@ -13,7 +13,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.team.bottles.core.designsystem.R -import com.team.bottles.core.designsystem.components.etc.UserInfo +import com.team.bottles.core.designsystem.components.etc.BottlesUserInfo import com.team.bottles.core.designsystem.theme.BottlesTheme import com.team.bottles.core.domain.profile.model.UserProfile import com.team.bottles.core.ui.CardProfile @@ -31,7 +31,7 @@ internal fun LazyListScope.introductionContents( item(key = "Introduction Contents") { when (isStoppedPingPong) { false -> { - UserInfo( + BottlesUserInfo( imageUrl = partnerProfile.imageUrl, userName = partnerProfile.userName, userAge = partnerProfile.age diff --git a/feat/profile/src/main/kotlin/com/team/bottles/feat/profile/introduction/IntroductionScreen.kt b/feat/profile/src/main/kotlin/com/team/bottles/feat/profile/introduction/IntroductionScreen.kt index 4825b530..50a977ce 100644 --- a/feat/profile/src/main/kotlin/com/team/bottles/feat/profile/introduction/IntroductionScreen.kt +++ b/feat/profile/src/main/kotlin/com/team/bottles/feat/profile/introduction/IntroductionScreen.kt @@ -75,9 +75,10 @@ internal fun IntroductionScreen( modifier = Modifier.background(color = BottlesTheme.color.background.primary), leadingIcon = { Icon( - modifier = Modifier.noRippleClickable( - onClick = { onIntent(IntroductionIntent.ClickBackButton) } - ), + modifier = Modifier + .noRippleClickable( + onClick = { onIntent(IntroductionIntent.ClickBackButton) } + ), painter = painterResource(id = R.drawable.ic_arrow_left_24), contentDescription = null, tint = BottlesTheme.color.icon.primary diff --git a/feat/profile/src/main/kotlin/com/team/bottles/feat/profile/introduction/component/SelecteImage.kt b/feat/profile/src/main/kotlin/com/team/bottles/feat/profile/introduction/component/SelecteImage.kt index 88b1705e..f7ed85d1 100644 --- a/feat/profile/src/main/kotlin/com/team/bottles/feat/profile/introduction/component/SelecteImage.kt +++ b/feat/profile/src/main/kotlin/com/team/bottles/feat/profile/introduction/component/SelecteImage.kt @@ -29,6 +29,7 @@ import com.skydoves.landscapist.coil.CoilImage import com.team.bottles.core.common.extension.toFile import com.team.bottles.core.designsystem.R import com.team.bottles.core.designsystem.components.buttons.BottlesIconButton +import com.team.bottles.core.designsystem.components.buttons.IconButtonType import com.team.bottles.core.designsystem.theme.BottlesTheme import com.team.bottles.feat.profile.introduction.mvi.IntroductionIntent import kotlinx.coroutines.Dispatchers @@ -87,6 +88,7 @@ internal fun SelectImageCard( .offset(x = (-16).dp, y = 16.dp) .align(Alignment.TopEnd), icon = R.drawable.ic_close_16, + iconButtonType = IconButtonType.RECTANGLE, onClick = { onIntent(IntroductionIntent.ClickDeleteButton) } ) } else { diff --git a/feat/report/src/main/kotlin/com/team/bottles/feat/report/ReportScreen.kt b/feat/report/src/main/kotlin/com/team/bottles/feat/report/ReportScreen.kt index 2db5bf7d..33c05914 100644 --- a/feat/report/src/main/kotlin/com/team/bottles/feat/report/ReportScreen.kt +++ b/feat/report/src/main/kotlin/com/team/bottles/feat/report/ReportScreen.kt @@ -1,6 +1,5 @@ package com.team.bottles.feat.report -import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsFocusedAsState @@ -26,11 +25,12 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import com.team.bottles.core.designsystem.R import com.team.bottles.core.designsystem.components.bars.BottlesTopBar -import com.team.bottles.core.designsystem.components.etc.UserInfo +import com.team.bottles.core.designsystem.components.etc.BottlesUserInfo import com.team.bottles.core.designsystem.components.textfield.BottlesLineTextFieldWithTrailingIcon import com.team.bottles.core.designsystem.modifier.noRippleClickable import com.team.bottles.core.designsystem.theme.BottlesTheme -import com.team.bottles.core.ui.BottlesAlertDialog +import com.team.bottles.core.ui.BottlesAlertDialogLeftDismissRightConfirm +import com.team.bottles.core.ui.model.AlertType import com.team.bottles.feat.report.components.ReportBottomBar import com.team.bottles.feat.report.mvi.ReportIntent import com.team.bottles.feat.report.mvi.ReportUiState @@ -49,13 +49,14 @@ internal fun ReportScreen( } if (uiState.showDialog) { - BottlesAlertDialog( + BottlesAlertDialogLeftDismissRightConfirm( onClose = { onIntent(ReportIntent.ClickDialogConfirm) }, onConfirm = { onIntent(ReportIntent.ClickDialogCancel) }, - confirmText = "신고하기", - dismissText = "계속하기", - title = "신고하기", - content = "접수 후 취소할 수 없으며 해당 사용자는 차단되요.\n정말 신고하시겠어요?" + onDismiss = { onIntent(ReportIntent.ClickDialogConfirm) }, + confirmButtonText = AlertType.USER_REPORT.confirmText, + dismissButtonText = AlertType.USER_REPORT.dismissText, + title = AlertType.USER_REPORT.title, + content = AlertType.USER_REPORT.content, ) } @@ -105,7 +106,7 @@ internal fun ReportScreen( Spacer(modifier = Modifier.height(height = BottlesTheme.spacing.doubleExtraLarge)) - UserInfo( + BottlesUserInfo( imageUrl = uiState.userImageUrl, userName = uiState.userName, userAge = uiState.userAge @@ -143,6 +144,7 @@ private fun ReportScreenPreview() { BottlesTheme { ReportScreen( uiState = ReportUiState( + showDialog = true, userName = "뇽뇽이", userAge = 15 ), diff --git a/feat/sandbeach/build.gradle.kts b/feat/sandbeach/build.gradle.kts index a21313f9..f7e81be0 100644 --- a/feat/sandbeach/build.gradle.kts +++ b/feat/sandbeach/build.gradle.kts @@ -8,6 +8,10 @@ plugins { android { namespace = "com.team.bottles.feat.sandbeach" + + defaultConfig { + buildConfigField("Integer", "VERSION_CODE", libs.versions.versionCode.get()) + } } dependencies { diff --git a/feat/sandbeach/src/main/kotlin/com/team/bottles/feat/sandbeach/SandBeachRoute.kt b/feat/sandbeach/src/main/kotlin/com/team/bottles/feat/sandbeach/SandBeachRoute.kt index fc327cd4..8a83e6f2 100644 --- a/feat/sandbeach/src/main/kotlin/com/team/bottles/feat/sandbeach/SandBeachRoute.kt +++ b/feat/sandbeach/src/main/kotlin/com/team/bottles/feat/sandbeach/SandBeachRoute.kt @@ -1,7 +1,10 @@ package com.team.bottles.feat.sandbeach import android.Manifest +import android.content.ActivityNotFoundException +import android.content.Intent import android.content.pm.PackageManager +import android.net.Uri import android.os.Build import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult @@ -29,19 +32,26 @@ internal fun SandBeachRoute( val context = LocalContext.current val notificationPermissionGranted = remember { - ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED + } else { + true + } } val permissionLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.RequestPermission() ) { isGranted -> if (isGranted) { + viewModel.confirmPermission() Toast.makeText(context, "알림에 동의 하였습니다.", Toast.LENGTH_SHORT).show() } } LaunchedEffect(notificationPermissionGranted) { if (!notificationPermissionGranted) { - permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + } } } @@ -51,6 +61,15 @@ internal fun SandBeachRoute( is SandBeachSideEffect.NavigateToIntroduction -> navigateToIntroduction() is SandBeachSideEffect.NavigateToArrivedBottle -> navigateToArrivedBottles() is SandBeachSideEffect.NavigateToBottleBox -> navigateToBottleBox() + is SandBeachSideEffect.NavigateToPlayStore -> { + try { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=com.team.bottles&hl=ko")) + context.startActivity(intent) + } catch (e: ActivityNotFoundException) { + val webIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=com.team.bottles&hl=ko")) + context.startActivity(webIntent) + } + } } } } diff --git a/feat/sandbeach/src/main/kotlin/com/team/bottles/feat/sandbeach/SandBeachScreen.kt b/feat/sandbeach/src/main/kotlin/com/team/bottles/feat/sandbeach/SandBeachScreen.kt index 0669a288..4ed26861 100644 --- a/feat/sandbeach/src/main/kotlin/com/team/bottles/feat/sandbeach/SandBeachScreen.kt +++ b/feat/sandbeach/src/main/kotlin/com/team/bottles/feat/sandbeach/SandBeachScreen.kt @@ -21,6 +21,7 @@ import com.team.bottles.core.designsystem.R import com.team.bottles.core.designsystem.components.bars.BottlesTopBar import com.team.bottles.core.designsystem.modifier.debounceNoRippleClickable import com.team.bottles.core.designsystem.theme.BottlesTheme +import com.team.bottles.core.ui.BottlesAlertConfirmDialog import com.team.bottles.feat.sandbeach.component.BottleStatusMessage import com.team.bottles.feat.sandbeach.component.InArrivedBottle import com.team.bottles.feat.sandbeach.component.InBottleBox @@ -35,6 +36,16 @@ internal fun SandBeachScreen( uiState: SandBeachUiState, onIntent: (SandBeachIntent) -> Unit ) { + if (uiState.showDialog) { + BottlesAlertConfirmDialog( + onClose = { /* 닫기 없음 */ }, + onConfirm = { onIntent(SandBeachIntent.ClickConfirmButton) }, + confirmButtonText = "업데이트 하기", + title = "업데이트 안내", + content = "최적의 사용 환경을 위해\n최신 버전의 앱으로 업데이트 해주세요", + ) + } + Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, @@ -107,7 +118,9 @@ private fun SandBeachScreenPreview() { contentScale = ContentScale.FillWidth ) SandBeachScreen( - uiState = SandBeachUiState(bottleStatus = BottleStatus.IN_ARRIVED_BOTTLE), + uiState = SandBeachUiState( + bottleStatus = BottleStatus.IN_ARRIVED_BOTTLE, + ), onIntent = {} ) } diff --git a/feat/sandbeach/src/main/kotlin/com/team/bottles/feat/sandbeach/SandBeachViewModel.kt b/feat/sandbeach/src/main/kotlin/com/team/bottles/feat/sandbeach/SandBeachViewModel.kt index fdf849b8..5235c73e 100644 --- a/feat/sandbeach/src/main/kotlin/com/team/bottles/feat/sandbeach/SandBeachViewModel.kt +++ b/feat/sandbeach/src/main/kotlin/com/team/bottles/feat/sandbeach/SandBeachViewModel.kt @@ -2,11 +2,14 @@ package com.team.bottles.feat.sandbeach import androidx.lifecycle.SavedStateHandle import com.team.bottles.core.common.BaseViewModel +import com.team.bottles.core.domain.auth.usecase.GetRequiredAppVersionUseCase import com.team.bottles.core.domain.bottle.usecase.GetBottleListUseCase import com.team.bottles.core.domain.bottle.usecase.GetPingPongListUseCase import com.team.bottles.core.domain.profile.model.UserProfileStatus -import com.team.bottles.core.domain.profile.usecase.GetUserIntroductionStatusUseCase import com.team.bottles.core.domain.profile.usecase.GetUserProfileStatusUseCase +import com.team.bottles.core.domain.user.model.Notification +import com.team.bottles.core.domain.user.model.NotificationType +import com.team.bottles.core.domain.user.usecase.UpdateSettingNotificationUseCase import com.team.bottles.feat.sandbeach.mvi.BottleStatus import com.team.bottles.feat.sandbeach.mvi.SandBeachIntent import com.team.bottles.feat.sandbeach.mvi.SandBeachSideEffect @@ -19,11 +22,20 @@ class SandBeachViewModel @Inject constructor( private val getUserProfileStatusUseCase: GetUserProfileStatusUseCase, private val getBottleListUseCase: GetBottleListUseCase, private val getPingPongListUseCase: GetPingPongListUseCase, + private val getRequiredAppVersionUseCase: GetRequiredAppVersionUseCase, + private val updateSettingNotificationUseCase: UpdateSettingNotificationUseCase, savedStateHandle: SavedStateHandle ) : BaseViewModel(savedStateHandle) { init { setSandBeachState() + launch { + val requiredAppVersion = getRequiredAppVersionUseCase() + + if (requiredAppVersion > currentState.appVersionCode) { + reduce { copy(showDialog = true) } + } + } } override fun createInitialState(savedStateHandle: SavedStateHandle): SandBeachUiState = @@ -84,6 +96,7 @@ class SandBeachViewModel @Inject constructor( when (intent) { is SandBeachIntent.ClickCreateIntroductionButton -> navigateToIntroduction() is SandBeachIntent.ClickSandBeach -> onClickSandBeach() + is SandBeachIntent.ClickConfirmButton -> navigateToPlayStore() } } @@ -108,4 +121,21 @@ class SandBeachViewModel @Inject constructor( postSideEffect(SandBeachSideEffect.NavigateToArrivedBottle) } + private fun navigateToPlayStore() { + postSideEffect(SandBeachSideEffect.NavigateToPlayStore) + } + + fun confirmPermission() { + launch { + NotificationType.entries.forEach { type -> + updateSettingNotificationUseCase( + notification = Notification( + notificationType = type, + enabled = true + ) + ) + } + } + } + } \ No newline at end of file diff --git a/feat/sandbeach/src/main/kotlin/com/team/bottles/feat/sandbeach/mvi/SandBeachIntent.kt b/feat/sandbeach/src/main/kotlin/com/team/bottles/feat/sandbeach/mvi/SandBeachIntent.kt index bd6d64f1..a32ba0c1 100644 --- a/feat/sandbeach/src/main/kotlin/com/team/bottles/feat/sandbeach/mvi/SandBeachIntent.kt +++ b/feat/sandbeach/src/main/kotlin/com/team/bottles/feat/sandbeach/mvi/SandBeachIntent.kt @@ -8,4 +8,6 @@ sealed interface SandBeachIntent : UiIntent { data object ClickSandBeach : SandBeachIntent + data object ClickConfirmButton : SandBeachIntent + } \ No newline at end of file diff --git a/feat/sandbeach/src/main/kotlin/com/team/bottles/feat/sandbeach/mvi/SandBeachSideEffect.kt b/feat/sandbeach/src/main/kotlin/com/team/bottles/feat/sandbeach/mvi/SandBeachSideEffect.kt index 7ab10f9b..3109ec55 100644 --- a/feat/sandbeach/src/main/kotlin/com/team/bottles/feat/sandbeach/mvi/SandBeachSideEffect.kt +++ b/feat/sandbeach/src/main/kotlin/com/team/bottles/feat/sandbeach/mvi/SandBeachSideEffect.kt @@ -10,4 +10,6 @@ sealed interface SandBeachSideEffect : UiSideEffect { data object NavigateToBottleBox : SandBeachSideEffect + data object NavigateToPlayStore : SandBeachSideEffect + } \ No newline at end of file diff --git a/feat/sandbeach/src/main/kotlin/com/team/bottles/feat/sandbeach/mvi/SandBeachUiState.kt b/feat/sandbeach/src/main/kotlin/com/team/bottles/feat/sandbeach/mvi/SandBeachUiState.kt index 4d3c4235..ac0ad550 100644 --- a/feat/sandbeach/src/main/kotlin/com/team/bottles/feat/sandbeach/mvi/SandBeachUiState.kt +++ b/feat/sandbeach/src/main/kotlin/com/team/bottles/feat/sandbeach/mvi/SandBeachUiState.kt @@ -1,12 +1,15 @@ package com.team.bottles.feat.sandbeach.mvi import com.team.bottles.core.common.UiState +import com.team.bottles.feat.sandbeach.BuildConfig data class SandBeachUiState( + val showDialog: Boolean = false, val bottleStatus: BottleStatus = BottleStatus.NONE_BOTTLE, val newBottleValue: Int = 0, val bottleBoxValue: Int = 0, - val afterArrivedTime: Int = 0 + val afterArrivedTime: Int = 0, + val appVersionCode: Int = BuildConfig.VERSION_CODE ): UiState enum class BottleStatus { @@ -16,8 +19,3 @@ enum class BottleStatus { NONE_BOTTLE, ; } - -// 자기소개 받아야 하는 상태 -// 도착한보틀 O, 보틀 보관함 X - 1순위 -// 도착한보틀 x, 보틀 보관함 O - 2순위 -// 도착한보틀 x, 보틀 보관함 x - 3순위 \ No newline at end of file diff --git a/feat/setting/.gitignore b/feat/setting/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feat/setting/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feat/setting/build.gradle.kts b/feat/setting/build.gradle.kts new file mode 100644 index 00000000..3be08d95 --- /dev/null +++ b/feat/setting/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("team.bottles.android.library.compose") + id("team.bottles.android.library") + id("team.bottles.android.feature") + id("team.bottles.android.hilt") + id("team.bottles.kotlin.serialization") +} + +android { + namespace = "com.team.bottles.feat.setting" +} + +dependencies { + +} \ No newline at end of file diff --git a/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/account/AccountSettingRoute.kt b/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/account/AccountSettingRoute.kt new file mode 100644 index 00000000..352affcb --- /dev/null +++ b/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/account/AccountSettingRoute.kt @@ -0,0 +1,32 @@ +package com.team.bottles.feat.setting.account + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.team.bottles.feat.setting.account.mvi.AccountSettingSideEffect +import com.team.bottles.feat.setting.notification.mvi.NotificationSideEffect + +@Composable +internal fun AccountSettingRoute( + viewModel: AccountSettingViewModel = hiltViewModel(), + navigateToLoginEndpoint: () -> Unit, + navigateToMyPage: () -> Unit, +) { + val uiState by viewModel.state.collectAsStateWithLifecycle() + + LaunchedEffect(Unit) { + viewModel.sideEffect.collect { sideEffect -> + when (sideEffect) { + is AccountSettingSideEffect.NavigateToLoginEndpoint -> navigateToLoginEndpoint() + is AccountSettingSideEffect.NavigateToMyPage -> navigateToMyPage() + } + } + } + + AccountSettingScreen( + uiState = uiState, + onIntent = { intent -> viewModel.intent(intent) }, + ) +} \ No newline at end of file diff --git a/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/account/AccountSettingScreen.kt b/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/account/AccountSettingScreen.kt new file mode 100644 index 00000000..f3e9efef --- /dev/null +++ b/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/account/AccountSettingScreen.kt @@ -0,0 +1,76 @@ +package com.team.bottles.feat.setting.account + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.team.bottles.core.designsystem.R +import com.team.bottles.core.designsystem.components.bars.BottlesTopBar +import com.team.bottles.core.designsystem.modifier.noRippleClickable +import com.team.bottles.core.designsystem.theme.BottlesTheme +import com.team.bottles.core.ui.BottlesAlertDialogLeftConfirmRightDismiss +import com.team.bottles.feat.setting.account.mvi.AccountSettingIntent +import com.team.bottles.feat.setting.account.mvi.AccountSettingUiState +import com.team.bottles.feat.setting.components.AccountSetting + +@Composable +internal fun AccountSettingScreen( + uiState: AccountSettingUiState, + onIntent: (AccountSettingIntent) -> Unit, +) { + if (uiState.showDialog) { + BottlesAlertDialogLeftConfirmRightDismiss( + onClose = { onIntent(AccountSettingIntent.ClickDismissDialogButton) }, + onDismiss = { onIntent(AccountSettingIntent.ClickDismissDialogButton) }, + onConfirm = { onIntent(AccountSettingIntent.ClickConfirmDialogButton) }, + confirmButtonText = uiState.dialogType.confirmText, + dismissButtonText = uiState.dialogType.dismissText, + title = uiState.dialogType.title, + content = uiState.dialogType.content + ) + } + + Column( + modifier = Modifier.fillMaxSize() + ) { + BottlesTopBar( + leadingIcon = { + Icon( + modifier = Modifier + .noRippleClickable(onClick = { onIntent(AccountSettingIntent.ClickBackButton) }), + painter = painterResource(id = R.drawable.ic_arrow_left_24), + contentDescription = null, + tint = BottlesTheme.color.icon.primary + ) + } + ) + + Spacer(modifier = Modifier.height(height = 32.dp)) + + AccountSetting( + modifier = Modifier.padding(horizontal = 16.dp), + isMatchingActive = uiState.isMatchingActive, + onChangeMatchingActive = { onIntent(AccountSettingIntent.ClickMatchingActiveToggleButton) }, + onClickLogOut = { onIntent(AccountSettingIntent.ClickLogOutButton) }, + onClickDeleteUser = { onIntent(AccountSettingIntent.ClickDeleteUserButton) }, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun AccountSettingScreenPreview() { + BottlesTheme { + AccountSettingScreen( + uiState = AccountSettingUiState(), + onIntent = {}, + ) + } +} \ No newline at end of file diff --git a/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/account/AccountSettingViewModel.kt b/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/account/AccountSettingViewModel.kt new file mode 100644 index 00000000..bda960fb --- /dev/null +++ b/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/account/AccountSettingViewModel.kt @@ -0,0 +1,84 @@ +package com.team.bottles.feat.setting.account + +import androidx.lifecycle.SavedStateHandle +import com.team.bottles.core.common.BaseViewModel +import com.team.bottles.core.domain.auth.usecase.DeleteUserUseCase +import com.team.bottles.core.domain.auth.usecase.LogOutUseCase +import com.team.bottles.feat.setting.account.mvi.AccountSettingIntent +import com.team.bottles.feat.setting.account.mvi.AccountSettingSideEffect +import com.team.bottles.feat.setting.account.mvi.AccountSettingUiState +import com.team.bottles.feat.setting.account.mvi.SettingAlertDialogType +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class AccountSettingViewModel @Inject constructor( + private val logOutUseCase: LogOutUseCase, + private val deleteUserUseCase: DeleteUserUseCase, + savedStateHandle: SavedStateHandle +) : BaseViewModel( + savedStateHandle +) { + override fun createInitialState(savedStateHandle: SavedStateHandle): AccountSettingUiState = + AccountSettingUiState() + + override fun handleClientException(throwable: Throwable) { + TODO("Not yet implemented") + } + + override suspend fun handleIntent(intent: AccountSettingIntent) { + when (intent) { + is AccountSettingIntent.ClickConfirmDialogButton -> confirm() + is AccountSettingIntent.ClickDismissDialogButton -> dismiss() + is AccountSettingIntent.ClickLogOutButton -> showLogoutDialog() + is AccountSettingIntent.ClickDeleteUserButton -> showDeleteUserDialog() + is AccountSettingIntent.ClickBackButton -> navigateToMyPage() + is AccountSettingIntent.ClickMatchingActiveToggleButton -> changeMatchingActive() + } + } + + private fun confirm() { + launch { + when (currentState.dialogType) { + SettingAlertDialogType.LOG_OUT -> logOutUseCase() + SettingAlertDialogType.DELETE_USER -> deleteUserUseCase() + } + reduce { copy(showDialog = false) } + postSideEffect(AccountSettingSideEffect.NavigateToLoginEndpoint) + } + } + + private fun dismiss() { + reduce { copy(showDialog = false) } + } + + private fun showLogoutDialog() { + reduce { + copy( + showDialog = true, + dialogType = SettingAlertDialogType.LOG_OUT + ) + } + } + + private fun showDeleteUserDialog() { + reduce { + copy( + showDialog = true, + dialogType = SettingAlertDialogType.DELETE_USER + ) + } + } + + private fun navigateToMyPage() { + postSideEffect(AccountSettingSideEffect.NavigateToMyPage) + } + + private fun changeMatchingActive() { + launch { + // TODO : 매칭 활성화 on/off 로직 + reduce { copy(isMatchingActive = !isMatchingActive) } + } + } + +} diff --git a/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/account/mvi/AccountSettingIntent.kt b/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/account/mvi/AccountSettingIntent.kt new file mode 100644 index 00000000..70994806 --- /dev/null +++ b/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/account/mvi/AccountSettingIntent.kt @@ -0,0 +1,19 @@ +package com.team.bottles.feat.setting.account.mvi + +import com.team.bottles.core.common.UiIntent + +sealed interface AccountSettingIntent : UiIntent { + + data object ClickBackButton : AccountSettingIntent + + data object ClickMatchingActiveToggleButton : AccountSettingIntent + + data object ClickLogOutButton : AccountSettingIntent + + data object ClickDeleteUserButton : AccountSettingIntent + + data object ClickConfirmDialogButton : AccountSettingIntent + + data object ClickDismissDialogButton : AccountSettingIntent + +} \ No newline at end of file diff --git a/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/account/mvi/AccountSettingSideEffect.kt b/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/account/mvi/AccountSettingSideEffect.kt new file mode 100644 index 00000000..36ed8182 --- /dev/null +++ b/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/account/mvi/AccountSettingSideEffect.kt @@ -0,0 +1,11 @@ +package com.team.bottles.feat.setting.account.mvi + +import com.team.bottles.core.common.UiSideEffect + +sealed interface AccountSettingSideEffect : UiSideEffect { + + data object NavigateToMyPage : AccountSettingSideEffect + + data object NavigateToLoginEndpoint : AccountSettingSideEffect + +} \ No newline at end of file diff --git a/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/account/mvi/AccountSettingUiState.kt b/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/account/mvi/AccountSettingUiState.kt new file mode 100644 index 00000000..8acd3f99 --- /dev/null +++ b/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/account/mvi/AccountSettingUiState.kt @@ -0,0 +1,31 @@ +package com.team.bottles.feat.setting.account.mvi + +import com.team.bottles.core.common.UiState + +data class AccountSettingUiState( + val showDialog: Boolean = false, + val dialogType: SettingAlertDialogType = SettingAlertDialogType.LOG_OUT, + val isMatchingActive: Boolean = false, +) : UiState + +enum class SettingAlertDialogType( + val title: String, + val content: String, + val dismissText: String, + val confirmText: String +) { + LOG_OUT( + title = "로그아웃", + content = "정말 로그아웃 하시겠어요?", + confirmText = "로그아웃하기", + dismissText = "취소하기" + ), + DELETE_USER( + title = "탈퇴하기", + content = "탈퇴 시 계정 복구가 어려워요.\n" + + "정말 탈퇴하시겠어요?", + confirmText = "탈퇴하기", + dismissText = "취소하기" + ), + ; +} diff --git a/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/components/AccountSetting.kt b/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/components/AccountSetting.kt new file mode 100644 index 00000000..6b80297b --- /dev/null +++ b/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/components/AccountSetting.kt @@ -0,0 +1,57 @@ +package com.team.bottles.feat.setting.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.team.bottles.core.designsystem.components.cards.BottlesCard +import com.team.bottles.core.designsystem.components.lists.BottlesSettingItemWithArrow +import com.team.bottles.core.designsystem.components.lists.BottlesSettingItemWithToggleButton +import com.team.bottles.core.designsystem.theme.BottlesTheme + +@Composable +internal fun AccountSetting( + modifier: Modifier = Modifier, + isMatchingActive: Boolean, + onChangeMatchingActive: () -> Unit, + onClickLogOut: () -> Unit, + onClickDeleteUser: () -> Unit, +) { + BottlesCard( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy( + space = BottlesTheme.spacing.large + ) + ) { +// BottlesSettingItemWithToggleButton( +// title = "매칭 활성화", +// subTitle = "비활성화 시 다른 사람을 추천 받을 수 없고\n" + +// "회원님도 다른 사람에게 추천되지 않아요", +// checked = isMatchingActive, +// onCheckedChange = onChangeMatchingActive +// ) TODO : 매칭 활성화 API 구현시 UI 수정 + + BottlesSettingItemWithArrow( + title = "로그아웃", + onClickItem = onClickLogOut + ) + + BottlesSettingItemWithArrow( + title = "탈퇴하기", + onClickItem = onClickDeleteUser + ) + } +} + +@Preview +@Composable +private fun AccountSettingPreview() { + BottlesTheme { + AccountSetting( + isMatchingActive = true, + onChangeMatchingActive = {}, + onClickDeleteUser = {}, + onClickLogOut = {} + ) + } +} \ No newline at end of file diff --git a/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/components/NotificationSetting.kt b/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/components/NotificationSetting.kt new file mode 100644 index 00000000..a5a591ec --- /dev/null +++ b/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/components/NotificationSetting.kt @@ -0,0 +1,82 @@ +package com.team.bottles.feat.setting.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.HorizontalDivider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.team.bottles.core.designsystem.components.cards.BottlesCard +import com.team.bottles.core.designsystem.components.lists.BottlesSettingItemWithToggleButton +import com.team.bottles.core.designsystem.theme.BottlesTheme + +@Composable +internal fun NotificationSetting( + modifier: Modifier = Modifier, + isFloatingBottle: Boolean, + isGoodFeelingArrived: Boolean, + isConversation: Boolean, + isMarketingResponse: Boolean, + onChangeFloatingBottle: () -> Unit, + onChangeGoodFeelingArrived: () -> Unit, + onChangeConversation: () -> Unit, + onChangeMarketingResponse: () -> Unit, +) { + BottlesCard( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy( + space = BottlesTheme.spacing.large + ) + ) { + BottlesSettingItemWithToggleButton( + title = "떠다니는 보틀 알림", + subTitle = "매일 랜덤으로 추천되는 보틀 안내", + checked = isFloatingBottle, + onCheckedChange = onChangeFloatingBottle + ) + + BottlesSettingItemWithToggleButton( + title = "호감 도착 알림", + subTitle = "내가 받은 호감 안내", + checked = isGoodFeelingArrived, + onCheckedChange = onChangeGoodFeelingArrived + ) + + BottlesSettingItemWithToggleButton( + title = "대화 알림", + subTitle = "가치관 문답 시작 · 진행 · 중단 , 매칭 안내", + checked = isConversation, + onCheckedChange = onChangeConversation + ) + + HorizontalDivider( + modifier = Modifier.fillMaxWidth(), + color = BottlesTheme.color.border.secondary, + thickness = 1.dp + ) + + BottlesSettingItemWithToggleButton( + title = "마케팅 수신 동의", + checked = isMarketingResponse, + onCheckedChange = onChangeMarketingResponse + ) + } +} + +@Preview +@Composable +private fun AccountSettingPreview() { + BottlesTheme { + NotificationSetting( + isFloatingBottle = true, + isGoodFeelingArrived = true, + isConversation = true, + isMarketingResponse = true, + onChangeFloatingBottle = {}, + onChangeGoodFeelingArrived = {}, + onChangeConversation = {}, + onChangeMarketingResponse = {} + ) + } +} \ No newline at end of file diff --git a/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/navigation/SettingNavigation.kt b/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/navigation/SettingNavigation.kt new file mode 100644 index 00000000..00b2f875 --- /dev/null +++ b/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/navigation/SettingNavigation.kt @@ -0,0 +1,29 @@ +package com.team.bottles.feat.setting.navigation + +import SettingNavigator +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import com.team.bottles.feat.setting.account.AccountSettingRoute +import com.team.bottles.feat.setting.notification.NotificationSettingRoute + +fun NavGraphBuilder.accountSettingScreen( + navigateToLoginEndpoint: () -> Unit, + navigateToMyPage: () -> Unit, +) { + composable { + AccountSettingRoute( + navigateToLoginEndpoint = navigateToLoginEndpoint, + navigateToMyPage = navigateToMyPage, + ) + } +} + +fun NavGraphBuilder.notificationSettingScreen( + navigateToMyPage: () -> Unit, +) { + composable { + NotificationSettingRoute( + navigateToMyPage = navigateToMyPage + ) + } +} \ No newline at end of file diff --git a/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/notification/NotificationSettingRoute.kt b/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/notification/NotificationSettingRoute.kt new file mode 100644 index 00000000..894d475c --- /dev/null +++ b/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/notification/NotificationSettingRoute.kt @@ -0,0 +1,29 @@ +package com.team.bottles.feat.setting.notification + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.team.bottles.feat.setting.notification.mvi.NotificationSideEffect + +@Composable +internal fun NotificationSettingRoute( + viewModel: NotificationSettingViewModel = hiltViewModel(), + navigateToMyPage: () -> Unit +) { + val uiState by viewModel.state.collectAsStateWithLifecycle() + + LaunchedEffect(Unit) { + viewModel.sideEffect.collect { sideEffect -> + if (sideEffect is NotificationSideEffect.NavigateToMyPage) { + navigateToMyPage() + } + } + } + + NotificationSettingScreen( + uiState = uiState, + onIntent = { intent -> viewModel.intent(intent) }, + ) +} \ No newline at end of file diff --git a/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/notification/NotificationSettingScreen.kt b/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/notification/NotificationSettingScreen.kt new file mode 100644 index 00000000..44881695 --- /dev/null +++ b/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/notification/NotificationSettingScreen.kt @@ -0,0 +1,67 @@ +package com.team.bottles.feat.setting.notification + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.team.bottles.core.designsystem.R +import com.team.bottles.core.designsystem.components.bars.BottlesTopBar +import com.team.bottles.core.designsystem.modifier.noRippleClickable +import com.team.bottles.core.designsystem.theme.BottlesTheme +import com.team.bottles.feat.setting.components.NotificationSetting +import com.team.bottles.feat.setting.notification.mvi.NotificationIntent +import com.team.bottles.feat.setting.notification.mvi.NotificationUiState + +@Composable +internal fun NotificationSettingScreen( + uiState: NotificationUiState, + onIntent: (NotificationIntent) -> Unit, +) { + Column( + modifier = Modifier.fillMaxSize() + ) { + BottlesTopBar( + leadingIcon = { + Icon( + modifier = Modifier + .noRippleClickable(onClick = { onIntent(NotificationIntent.ClickBackButton) }), + painter = painterResource(id = R.drawable.ic_arrow_left_24), + contentDescription = null, + tint = BottlesTheme.color.icon.primary + ) + }, + ) + + Spacer(modifier = Modifier.height(height = 32.dp)) + + NotificationSetting( + modifier = Modifier.padding(horizontal = 16.dp), + isFloatingBottle = uiState.isFloatingBottle, + isGoodFeelingArrived = uiState.isGoodFeelingArrived, + isConversation = uiState.isConversation, + isMarketingResponse = uiState.isMarketingResponse, + onChangeFloatingBottle = { onIntent(NotificationIntent.ClickFloatingBottleToggleButton) }, + onChangeGoodFeelingArrived = { onIntent(NotificationIntent.ClickGoodFeelingArrivedToggleButton) }, + onChangeConversation = { onIntent(NotificationIntent.ClickConversationToggleButton) }, + onChangeMarketingResponse = { onIntent(NotificationIntent.ClickMarketingResponseToggleButton) }, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun NotificationSettingScreenPreview() { + BottlesTheme { + NotificationSettingScreen( + uiState = NotificationUiState(), + onIntent = {}, + ) + } +} \ No newline at end of file diff --git a/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/notification/NotificationSettingViewModel.kt b/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/notification/NotificationSettingViewModel.kt new file mode 100644 index 00000000..efe788cb --- /dev/null +++ b/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/notification/NotificationSettingViewModel.kt @@ -0,0 +1,111 @@ +package com.team.bottles.feat.setting.notification + +import androidx.lifecycle.SavedStateHandle +import com.team.bottles.core.common.BaseViewModel +import com.team.bottles.core.domain.user.model.Notification +import com.team.bottles.core.domain.user.model.NotificationType +import com.team.bottles.core.domain.user.usecase.GetSettingNotificationsUseCase +import com.team.bottles.core.domain.user.usecase.UpdateSettingNotificationUseCase +import com.team.bottles.feat.setting.notification.mvi.NotificationIntent +import com.team.bottles.feat.setting.notification.mvi.NotificationSideEffect +import com.team.bottles.feat.setting.notification.mvi.NotificationUiState +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class NotificationSettingViewModel @Inject constructor( + private val getSettingNotificationsUseCase: GetSettingNotificationsUseCase, + private val updateSettingNotificationUseCase: UpdateSettingNotificationUseCase, + savedStateHandle: SavedStateHandle +) : BaseViewModel(savedStateHandle) { + + init { + launch { + val notifications = getSettingNotificationsUseCase() + + val isConversation = notifications.find { it.notificationType == NotificationType.PING_PONG }?.enabled?: false + val isMarketingResponse = notifications.find { it.notificationType == NotificationType.MARKETING }?.enabled?: false + val isFloatingBottle = notifications.find { it.notificationType == NotificationType.DAILY_RANDOM }?.enabled?: false + val isGoodFeelingArrived = notifications.find { it.notificationType == NotificationType.RECEIVE_LIKE }?.enabled?: false + + reduce { + copy( + isConversation = isConversation, + isMarketingResponse = isMarketingResponse, + isFloatingBottle = isFloatingBottle, + isGoodFeelingArrived = isGoodFeelingArrived + ) + } + } + } + + override fun createInitialState(savedStateHandle: SavedStateHandle): NotificationUiState = + NotificationUiState() + + override suspend fun handleIntent(intent: NotificationIntent) { + when (intent) { + is NotificationIntent.ClickBackButton -> navigateToMyPage() + is NotificationIntent.ClickConversationToggleButton -> changeConversationNotification() + is NotificationIntent.ClickFloatingBottleToggleButton -> changeFloatingBottleNotification() + is NotificationIntent.ClickMarketingResponseToggleButton -> changeMarketingResponseNotification() + is NotificationIntent.ClickGoodFeelingArrivedToggleButton -> changeGoodFeelingArrivedNotification() + } + } + + override fun handleClientException(throwable: Throwable) { + TODO("Not yet implemented") + } + + private fun navigateToMyPage() { + postSideEffect(NotificationSideEffect.NavigateToMyPage) + } + + private fun changeFloatingBottleNotification() { + launch { + updateSettingNotificationUseCase( + notification = Notification( + notificationType = NotificationType.DAILY_RANDOM, + enabled = !currentState.isFloatingBottle + ) + ) + reduce { copy(isFloatingBottle = !isFloatingBottle) } + } + } + + private fun changeGoodFeelingArrivedNotification() { + launch { + updateSettingNotificationUseCase( + notification = Notification( + notificationType = NotificationType.RECEIVE_LIKE, + enabled = !currentState.isGoodFeelingArrived + ) + ) + reduce { copy(isGoodFeelingArrived = !isGoodFeelingArrived) } + } + } + + private fun changeConversationNotification() { + launch { + updateSettingNotificationUseCase( + notification = Notification( + notificationType = NotificationType.PING_PONG, + enabled = !currentState.isConversation + ) + ) + reduce { copy(isConversation = !isConversation) } + } + } + + private fun changeMarketingResponseNotification() { + launch { + updateSettingNotificationUseCase( + notification = Notification( + notificationType = NotificationType.MARKETING, + enabled = !currentState.isMarketingResponse + ) + ) + reduce { copy(isMarketingResponse = !isMarketingResponse) } + } + } + +} \ No newline at end of file diff --git a/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/notification/mvi/NotificationIntent.kt b/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/notification/mvi/NotificationIntent.kt new file mode 100644 index 00000000..4a265249 --- /dev/null +++ b/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/notification/mvi/NotificationIntent.kt @@ -0,0 +1,17 @@ +package com.team.bottles.feat.setting.notification.mvi + +import com.team.bottles.core.common.UiIntent + +sealed interface NotificationIntent : UiIntent { + + data object ClickBackButton : NotificationIntent + + data object ClickFloatingBottleToggleButton : NotificationIntent + + data object ClickGoodFeelingArrivedToggleButton : NotificationIntent + + data object ClickConversationToggleButton : NotificationIntent + + data object ClickMarketingResponseToggleButton : NotificationIntent + +} \ No newline at end of file diff --git a/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/notification/mvi/NotificationSideEffect.kt b/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/notification/mvi/NotificationSideEffect.kt new file mode 100644 index 00000000..fa30bddf --- /dev/null +++ b/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/notification/mvi/NotificationSideEffect.kt @@ -0,0 +1,9 @@ +package com.team.bottles.feat.setting.notification.mvi + +import com.team.bottles.core.common.UiSideEffect + +sealed interface NotificationSideEffect : UiSideEffect { + + data object NavigateToMyPage : NotificationSideEffect + +} \ No newline at end of file diff --git a/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/notification/mvi/NotificationUiState.kt b/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/notification/mvi/NotificationUiState.kt new file mode 100644 index 00000000..b6b4d47e --- /dev/null +++ b/feat/setting/src/main/kotlin/com/team/bottles/feat/setting/notification/mvi/NotificationUiState.kt @@ -0,0 +1,10 @@ +package com.team.bottles.feat.setting.notification.mvi + +import com.team.bottles.core.common.UiState + +data class NotificationUiState( + val isFloatingBottle: Boolean = false, + val isGoodFeelingArrived: Boolean = false, + val isConversation: Boolean = false, + val isMarketingResponse: Boolean = false, +) : UiState diff --git a/feat/splash/build.gradle.kts b/feat/splash/build.gradle.kts index f34690b0..e6b413a3 100644 --- a/feat/splash/build.gradle.kts +++ b/feat/splash/build.gradle.kts @@ -8,6 +8,10 @@ plugins { android { namespace = "com.team.bottles.feat.splash" + + defaultConfig { + buildConfigField("Integer", "VERSION_CODE", libs.versions.versionCode.get()) + } } dependencies { diff --git a/feat/splash/src/main/kotlin/com/team/bottles/feat/splash/SplashRoute.kt b/feat/splash/src/main/kotlin/com/team/bottles/feat/splash/SplashRoute.kt index b17e6ef5..9e808326 100644 --- a/feat/splash/src/main/kotlin/com/team/bottles/feat/splash/SplashRoute.kt +++ b/feat/splash/src/main/kotlin/com/team/bottles/feat/splash/SplashRoute.kt @@ -1,8 +1,15 @@ package com.team.bottles.feat.splash +import android.content.ActivityNotFoundException +import android.content.Intent +import android.net.Uri import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.LocalContext import androidx.hilt.navigation.compose.hiltViewModel +import com.team.bottles.feat.splash.mvi.SplashSideEffect @Composable fun SplashRoute( @@ -11,15 +18,30 @@ fun SplashRoute( navigateToSandBeach: () -> Unit, navigateToOnboarding: () -> Unit ) { + val uiState by viewModel.state.collectAsState() + val context = LocalContext.current + LaunchedEffect(Unit) { viewModel.sideEffect.collect { sideEffect -> when (sideEffect) { is SplashSideEffect.NavigateToSandBeach -> navigateToSandBeach() is SplashSideEffect.NavigateToLoginEndpoint -> navigateToLoginEndpoint() is SplashSideEffect.NavigateToOnboarding -> navigateToOnboarding() + is SplashSideEffect.NavigateToPlayStore -> { + try { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=com.team.bottles&hl=ko")) + context.startActivity(intent) + } catch (e: ActivityNotFoundException) { + val webIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=com.team.bottles&hl=ko")) + context.startActivity(webIntent) + } + } } } } - SplashScreen() + SplashScreen( + uiState = uiState, + onIntent = { intent -> viewModel.intent(intent = intent) } + ) } diff --git a/feat/splash/src/main/kotlin/com/team/bottles/feat/splash/SplashScreen.kt b/feat/splash/src/main/kotlin/com/team/bottles/feat/splash/SplashScreen.kt index 98c1a597..71d200ae 100644 --- a/feat/splash/src/main/kotlin/com/team/bottles/feat/splash/SplashScreen.kt +++ b/feat/splash/src/main/kotlin/com/team/bottles/feat/splash/SplashScreen.kt @@ -15,9 +15,25 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.team.bottles.core.designsystem.R import com.team.bottles.core.designsystem.theme.BottlesTheme +import com.team.bottles.core.ui.BottlesAlertConfirmDialog +import com.team.bottles.feat.splash.mvi.SplashIntent +import com.team.bottles.feat.splash.mvi.SplashUiState @Composable -internal fun SplashScreen() { +internal fun SplashScreen( + uiState: SplashUiState, + onIntent: (SplashIntent) -> Unit +) { + if (uiState.showDialog) { + BottlesAlertConfirmDialog( + onClose = { /* 닫기 없음 */ }, + onConfirm = { onIntent(SplashIntent.ClickConfirmButton) }, + confirmButtonText = "업데이트 하기", + title = "업데이트 안내", + content = "최적의 사용 환경을 위해\n최신 버전의 앱으로 업데이트 해주세요", + ) + } + Column( modifier = Modifier .fillMaxSize() @@ -45,6 +61,11 @@ internal fun SplashScreen() { @Composable private fun SplashScreenPreview() { BottlesTheme { - SplashScreen() + SplashScreen( + uiState = SplashUiState( + showDialog = true + ), + onIntent = {} + ) } } \ No newline at end of file diff --git a/feat/splash/src/main/kotlin/com/team/bottles/feat/splash/SplashSideEffect.kt b/feat/splash/src/main/kotlin/com/team/bottles/feat/splash/SplashSideEffect.kt deleted file mode 100644 index fc0ab698..00000000 --- a/feat/splash/src/main/kotlin/com/team/bottles/feat/splash/SplashSideEffect.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.team.bottles.feat.splash - -sealed interface SplashSideEffect { - - data object NavigateToLoginEndpoint : SplashSideEffect - - data object NavigateToSandBeach : SplashSideEffect - - data object NavigateToOnboarding : SplashSideEffect - -} \ No newline at end of file diff --git a/feat/splash/src/main/kotlin/com/team/bottles/feat/splash/SplashViewModel.kt b/feat/splash/src/main/kotlin/com/team/bottles/feat/splash/SplashViewModel.kt index 8de27eef..c04507b1 100644 --- a/feat/splash/src/main/kotlin/com/team/bottles/feat/splash/SplashViewModel.kt +++ b/feat/splash/src/main/kotlin/com/team/bottles/feat/splash/SplashViewModel.kt @@ -1,43 +1,62 @@ package com.team.bottles.feat.splash -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope +import androidx.lifecycle.SavedStateHandle +import com.team.bottles.core.common.BaseViewModel +import com.team.bottles.core.domain.auth.usecase.GetRequiredAppVersionUseCase import com.team.bottles.core.domain.auth.usecase.WebViewConnectUseCase import com.team.bottles.core.domain.profile.model.UserProfileStatus import com.team.bottles.core.domain.profile.usecase.GetUserProfileStatusUseCase +import com.team.bottles.feat.splash.mvi.SplashIntent +import com.team.bottles.feat.splash.mvi.SplashSideEffect +import com.team.bottles.feat.splash.mvi.SplashUiState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class SplashViewModel @Inject constructor( private val getTokenStatus: WebViewConnectUseCase, - private val getUserProfileStatusUseCase: GetUserProfileStatusUseCase -): ViewModel() { - - private val _sideEffect: MutableSharedFlow = MutableSharedFlow() - val sideEffect = _sideEffect.asSharedFlow() + private val getUserProfileStatusUseCase: GetUserProfileStatusUseCase, + private val getRequiredAppVersionUseCase: GetRequiredAppVersionUseCase, + savedStateHandle: SavedStateHandle, +): BaseViewModel(savedStateHandle) { init { - viewModelScope.launch { - val tokens = getTokenStatus.getLocalToken() - - delay(1000L) + launch { + val requiredAppVersion = getRequiredAppVersionUseCase() - if (tokens.accessToken.isEmpty()) { - _sideEffect.emit(SplashSideEffect.NavigateToLoginEndpoint) + if (requiredAppVersion > currentState.appVersionCode) { + reduce { copy(showDialog = true) } } else { - val profileStatus = getUserProfileStatusUseCase() + val tokens = getTokenStatus.getLocalToken() + + delay(1000L) - when (profileStatus) { - UserProfileStatus.EMPTY -> _sideEffect.emit(SplashSideEffect.NavigateToOnboarding) - else -> _sideEffect.emit(SplashSideEffect.NavigateToSandBeach) + if (tokens.accessToken.isEmpty()) { + postSideEffect(SplashSideEffect.NavigateToLoginEndpoint) + } else { + val profileStatus = getUserProfileStatusUseCase() + + when (profileStatus) { + UserProfileStatus.EMPTY -> postSideEffect(SplashSideEffect.NavigateToOnboarding) + else -> postSideEffect(SplashSideEffect.NavigateToSandBeach) + } } } } } + override fun createInitialState(savedStateHandle: SavedStateHandle): SplashUiState = + SplashUiState() + + override suspend fun handleIntent(intent: SplashIntent) { + when (intent) { + is SplashIntent.ClickConfirmButton -> postSideEffect(SplashSideEffect.NavigateToPlayStore) + } + } + + override fun handleClientException(throwable: Throwable) { + TODO("Not yet implemented") + } + } \ No newline at end of file diff --git a/feat/splash/src/main/kotlin/com/team/bottles/feat/splash/mvi/SplashIntent.kt b/feat/splash/src/main/kotlin/com/team/bottles/feat/splash/mvi/SplashIntent.kt new file mode 100644 index 00000000..718ad8a1 --- /dev/null +++ b/feat/splash/src/main/kotlin/com/team/bottles/feat/splash/mvi/SplashIntent.kt @@ -0,0 +1,9 @@ +package com.team.bottles.feat.splash.mvi + +import com.team.bottles.core.common.UiIntent + +sealed interface SplashIntent : UiIntent { + + data object ClickConfirmButton : SplashIntent + +} \ No newline at end of file diff --git a/feat/splash/src/main/kotlin/com/team/bottles/feat/splash/mvi/SplashSideEffect.kt b/feat/splash/src/main/kotlin/com/team/bottles/feat/splash/mvi/SplashSideEffect.kt new file mode 100644 index 00000000..a7e8899f --- /dev/null +++ b/feat/splash/src/main/kotlin/com/team/bottles/feat/splash/mvi/SplashSideEffect.kt @@ -0,0 +1,15 @@ +package com.team.bottles.feat.splash.mvi + +import com.team.bottles.core.common.UiSideEffect + +sealed interface SplashSideEffect : UiSideEffect { + + data object NavigateToLoginEndpoint : SplashSideEffect + + data object NavigateToSandBeach : SplashSideEffect + + data object NavigateToOnboarding : SplashSideEffect + + data object NavigateToPlayStore : SplashSideEffect + +} \ No newline at end of file diff --git a/feat/splash/src/main/kotlin/com/team/bottles/feat/splash/mvi/SplashUiState.kt b/feat/splash/src/main/kotlin/com/team/bottles/feat/splash/mvi/SplashUiState.kt new file mode 100644 index 00000000..d4efe7b9 --- /dev/null +++ b/feat/splash/src/main/kotlin/com/team/bottles/feat/splash/mvi/SplashUiState.kt @@ -0,0 +1,9 @@ +package com.team.bottles.feat.splash.mvi + +import com.team.bottles.core.common.UiState +import com.team.bottles.feat.splash.BuildConfig + +data class SplashUiState( + val appVersionCode: Int = BuildConfig.VERSION_CODE, + val showDialog: Boolean = false, +) : UiState \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9208720b..d6511f3f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,9 +3,9 @@ compileSdk = "34" minSdk = "28" targetSdk = "34" -appVersion = "0.9.5-beta" -versionCode = "10007" -versionName = "0.9.5" +appVersion = "1.0.0" +versionCode = "10008" +versionName = "1.0.0" # kotlin diff --git a/settings.gradle.kts b/settings.gradle.kts index 2f093c55..daa59b0f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -34,6 +34,7 @@ include(":core:datastore") include(":core:ui") include(":core:navigator") include(":core:common") +include(":core:local") include(":feat:bottle") include(":feat:sandbeach") @@ -43,4 +44,5 @@ include(":feat:onboarding") include(":feat:profile") include(":feat:ping-pong") include(":feat:splash") -include(":feat:report") \ No newline at end of file +include(":feat:report") +include(":feat:setting") \ No newline at end of file