diff --git a/core/common/src/main/java/com/example/common/constant/Url.kt b/core/common/src/main/java/com/example/common/constant/Url.kt new file mode 100644 index 00000000..97651853 --- /dev/null +++ b/core/common/src/main/java/com/example/common/constant/Url.kt @@ -0,0 +1,21 @@ +package com.example.common.constant + +object Url { + // 서비스 이용 약관 + const val TERMS_OF_SERVICE = "https://www.notion.so/2d13aeb558c9801fb8c2db2ae6ac2c3e?source=copy_link" + + // 개인정보 처리 방침 + const val PRIVACY_POLICY = "https://www.notion.so/2d13aeb558c98003b480f83b06245430?source=copy_link" + + // 공지사항 + const val ANNOUNCEMENT = "https://www.notion.so/2d13aeb558c980919796c2b4d7109369?source=copy_link" + + // 커뮤니티 가이드 + const val COMMUNITY_GUIDE = "https://www.notion.so/2d13aeb558c980c7915bf540db799aac?source=copy_link" + + // 문의/제안하기 + const val INQUIRY = "https://forms.gle/reQb2nmhjSqXVvnq7" + + // 에러 뷰 + const val ERROR = "" +} diff --git a/core/common/src/main/java/com/example/common/type/TermType.kt b/core/common/src/main/java/com/example/common/type/TermType.kt index 268c60fb..722245ae 100644 --- a/core/common/src/main/java/com/example/common/type/TermType.kt +++ b/core/common/src/main/java/com/example/common/type/TermType.kt @@ -1,10 +1,13 @@ package com.example.common.type +import com.example.common.constant.Url + enum class TermType( val isMandatory: Boolean, + val url: String, ) { - TERMS_OF_SERVICE(isMandatory = true), // 서비스 이용약관 (필수) - PRIVACY_POLICY(isMandatory = true), // 개인정보 처리방침 (필수) + TERMS_OF_SERVICE(isMandatory = true, url = Url.TERMS_OF_SERVICE), // 서비스 이용약관 (필수) + PRIVACY_POLICY(isMandatory = true, url = Url.PRIVACY_POLICY), // 개인정보 처리방침 (필수) ; companion object { diff --git a/core/data/src/main/java/com/example/data/constant/ErrorCode.kt b/core/data/src/main/java/com/example/data/constant/ErrorCode.kt index 6693d4c6..58f6a217 100644 --- a/core/data/src/main/java/com/example/data/constant/ErrorCode.kt +++ b/core/data/src/main/java/com/example/data/constant/ErrorCode.kt @@ -3,4 +3,5 @@ package com.example.data.constant object ErrorCode { const val USER_NOT_FOUND = 4041 const val EXPIRED_ACCESS_TOKEN = 4012 + const val DUPLICATED_NICKNAME = 4090 } diff --git a/core/data/src/main/java/com/example/data/datasource/remote/AuthRemoteDataSource.kt b/core/data/src/main/java/com/example/data/datasource/remote/AuthRemoteDataSource.kt index 793b3e5e..dc18566a 100644 --- a/core/data/src/main/java/com/example/data/datasource/remote/AuthRemoteDataSource.kt +++ b/core/data/src/main/java/com/example/data/datasource/remote/AuthRemoteDataSource.kt @@ -15,6 +15,7 @@ import okhttp3.MultipartBody import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.toRequestBody import retrofit2.HttpException +import timber.log.Timber import java.io.File import java.net.HttpURLConnection import javax.inject.Inject @@ -84,7 +85,25 @@ class AuthRemoteDataSource ) return response.data ?: throw Exception("Data is null") - } catch (e: Exception) { + } catch (e: HttpException) { + if (e.code() == HttpURLConnection.HTTP_CONFLICT) { + val errorString = e.response()?.errorBody()?.string() + Timber.d("errorString : $errorString") + + if (errorString != null) { + try { + val errorResponse = json.decodeFromString>(errorString) + Timber.d("errorResponse : $errorResponse") + if (errorResponse.code == ErrorCode.DUPLICATED_NICKNAME) { + throw NetworkException(ErrorCode.DUPLICATED_NICKNAME, errorResponse.message) + } + } catch (e: SerializationException) { + // JSON 형식이 잘못됨 (괄호 누락 등) + } catch (e: IllegalArgumentException) { + // 데이터 타입 불일치 + } + } + } throw e } } diff --git a/core/data/src/main/java/com/example/data/datasource/remote/UserRemoteDataSource.kt b/core/data/src/main/java/com/example/data/datasource/remote/UserRemoteDataSource.kt index 2276ba61..6df79ff5 100644 --- a/core/data/src/main/java/com/example/data/datasource/remote/UserRemoteDataSource.kt +++ b/core/data/src/main/java/com/example/data/datasource/remote/UserRemoteDataSource.kt @@ -1,6 +1,7 @@ package com.example.data.datasource.remote import com.example.data.model.request.NotificationRequest +import com.example.data.model.request.PatchProfileRequest import com.example.data.model.response.UserInfoResponse import com.example.data.service.UserService import kotlinx.serialization.InternalSerializationApi @@ -32,7 +33,10 @@ class UserRemoteDataSource userService.patchProfile( profileImg = imagePart, - request = nickname, + request = + PatchProfileRequest( + nickname, + ), ) } catch (e: Exception) { Timber.e(e) diff --git a/core/data/src/main/java/com/example/data/mapper/todomain/UserResponseMapper.kt b/core/data/src/main/java/com/example/data/mapper/todomain/UserResponseMapper.kt index b4176816..85c7c5b5 100644 --- a/core/data/src/main/java/com/example/data/mapper/todomain/UserResponseMapper.kt +++ b/core/data/src/main/java/com/example/data/mapper/todomain/UserResponseMapper.kt @@ -7,5 +7,5 @@ fun UserResponse.toDomain(): Writer = Writer( userId = this.userId, nickname = this.nickname, - profileImg = this.profileImg ?: "", + profileImg = this.profileImg, ) diff --git a/core/data/src/main/java/com/example/data/model/request/PatchProfileRequest.kt b/core/data/src/main/java/com/example/data/model/request/PatchProfileRequest.kt new file mode 100644 index 00000000..f8c25058 --- /dev/null +++ b/core/data/src/main/java/com/example/data/model/request/PatchProfileRequest.kt @@ -0,0 +1,8 @@ +package com.example.data.model.request + +import kotlinx.serialization.Serializable + +@Serializable +data class PatchProfileRequest( + val nickname: String?, +) diff --git a/core/data/src/main/java/com/example/data/repository/AuthRepositoryImpl.kt b/core/data/src/main/java/com/example/data/repository/AuthRepositoryImpl.kt index 5958950a..aa0f253f 100644 --- a/core/data/src/main/java/com/example/data/repository/AuthRepositoryImpl.kt +++ b/core/data/src/main/java/com/example/data/repository/AuthRepositoryImpl.kt @@ -10,6 +10,7 @@ import com.example.data.datasource.remote.KakaoLoginDataSource import com.example.data.datasource.remote.UserRemoteDataSource import com.example.data.model.request.LoginRequest import com.example.data.model.request.SignupRequest +import com.example.domain.model.NicknameValidationResult import com.example.domain.model.User import com.example.domain.repository.AuthRepository import com.example.network.NetworkException @@ -64,21 +65,27 @@ class AuthRepositoryImpl kakaoAccessToken: String?, profileImage: String?, nickname: String, - ): Result = + ): Result = runCatching { val profileFile = fileLocalDataSource.createAndGetFile(profileImage) val response = - authRemoteDataSource.signup( - kakaoAccessToken = kakaoAccessToken, - imageFile = profileFile, - signupRequest = - SignupRequest( - platform = KAKAO_PLATFORM, - nickname = nickname, - ), - ) - + try { + authRemoteDataSource.signup( + kakaoAccessToken = kakaoAccessToken, + imageFile = profileFile, + signupRequest = + SignupRequest( + platform = KAKAO_PLATFORM, + nickname = nickname, + ), + ) + } catch (e: NetworkException) { + if (e.code == ErrorCode.DUPLICATED_NICKNAME) { + return Result.success(NicknameValidationResult.Error.Duplicated) + } + throw e + } tokenLocalDataSource.saveTokens( accessToken = response.accessToken, refreshToken = response.refreshToken, @@ -91,6 +98,8 @@ class AuthRepositoryImpl profileImagePath = profileFile?.absolutePath, ), ) + + return Result.success(NicknameValidationResult.Success) } override suspend fun withdraw(): Result = diff --git a/core/data/src/main/java/com/example/data/service/UserService.kt b/core/data/src/main/java/com/example/data/service/UserService.kt index 4e6b4638..a6fe00a9 100644 --- a/core/data/src/main/java/com/example/data/service/UserService.kt +++ b/core/data/src/main/java/com/example/data/service/UserService.kt @@ -8,6 +8,7 @@ import com.example.data.constant.ApiConstants.SCRAPS import com.example.data.constant.ApiConstants.USERS import com.example.data.constant.ApiConstants.VERSIONS import com.example.data.model.request.NotificationRequest +import com.example.data.model.request.PatchProfileRequest import com.example.data.model.response.BaseResponse import com.example.data.model.response.NotificationResponse import com.example.data.model.response.RegisteredTracksResponse @@ -30,7 +31,7 @@ interface UserService { @PATCH("$API/$VERSIONS/$USERS/$ME") suspend fun patchProfile( @Part profileImg: MultipartBody.Part?, - @Part("nickname") request: String?, + @Part("changeProfileRequest") request: PatchProfileRequest, ): BaseResponse @GET("$API/$VERSIONS/$USERS/{userId}") diff --git a/core/designsystem/src/main/java/com/example/designsystem/component/DPlayLargeCover.kt b/core/designsystem/src/main/java/com/example/designsystem/component/DPlayLargeCover.kt index 52aeaf4c..61d13938 100644 --- a/core/designsystem/src/main/java/com/example/designsystem/component/DPlayLargeCover.kt +++ b/core/designsystem/src/main/java/com/example/designsystem/component/DPlayLargeCover.kt @@ -40,7 +40,7 @@ fun DPlayLargeCover( isBookmarkChecked: Boolean, isLikeChecked: Boolean, likeCount: Int, - writerProfileImageUrl: String, + writerProfileImageUrl: String?, writerNickname: String, content: String, musicImageUrl: String, @@ -130,7 +130,7 @@ fun DPlayLargeCover( modifier = Modifier.noRippleClickable(onClick = onWriterProfileClick), ) { AsyncImage( - model = writerProfileImageUrl, + model = writerProfileImageUrl ?: R.drawable.base_profile_image, contentDescription = null, modifier = Modifier diff --git a/core/designsystem/src/main/java/com/example/designsystem/component/DPlayProfileImageArea.kt b/core/designsystem/src/main/java/com/example/designsystem/component/DPlayProfileImageArea.kt new file mode 100644 index 00000000..e05e4d2f --- /dev/null +++ b/core/designsystem/src/main/java/com/example/designsystem/component/DPlayProfileImageArea.kt @@ -0,0 +1,50 @@ +package com.example.designsystem.component + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import com.dplay.designsystem.R +import com.example.designsystem.theme.DPlayTheme +import com.example.designsystem.util.noRippleClickable + +@Composable +fun DPlayProfileImageArea( + onProfileImageClick: () -> Unit, + profileImagePath: String?, + modifier: Modifier = Modifier, + content: @Composable () -> Unit = {}, +) { + Box( + modifier = + modifier + .noRippleClickable( + onClick = { onProfileImageClick() }, + ), + contentAlignment = Alignment.BottomEnd, + ) { + AsyncImage( + model = profileImagePath ?: R.drawable.base_profile_image, + contentDescription = null, + modifier = + Modifier + .fillMaxSize() + .clip(CircleShape) + .border( + width = 1.dp, + color = DPlayTheme.colors.gray200, + shape = CircleShape, + ), + contentScale = ContentScale.Crop, + ) + + content() + } +} diff --git a/core/designsystem/src/main/res/drawable/base_profile_image.png b/core/designsystem/src/main/res/drawable/base_profile_image.png new file mode 100644 index 00000000..27f153b1 Binary files /dev/null and b/core/designsystem/src/main/res/drawable/base_profile_image.png differ diff --git a/core/designsystem/src/main/res/values/strings.xml b/core/designsystem/src/main/res/values/strings.xml index f84bfc2a..0b0e69f5 100644 --- a/core/designsystem/src/main/res/values/strings.xml +++ b/core/designsystem/src/main/res/values/strings.xml @@ -31,7 +31,7 @@ %d자 이상 입력해주세요 특수문자, 띄어쓰기는 사용 불가능해요 - 이미 사용 중인 값입니다 + 이미 사용 중인 닉네임이에요 비속어, 금칙어가 포함되어 있습니다 사용 가능한 닉네임이에요 diff --git a/core/domain/src/main/java/com/example/domain/model/NicknameValidationResult.kt b/core/domain/src/main/java/com/example/domain/model/NicknameValidationResult.kt index 2c03f7af..a8106cd0 100644 --- a/core/domain/src/main/java/com/example/domain/model/NicknameValidationResult.kt +++ b/core/domain/src/main/java/com/example/domain/model/NicknameValidationResult.kt @@ -6,5 +6,6 @@ sealed interface NicknameValidationResult { sealed interface Error : NicknameValidationResult { data object TooShort : Error data object InvalidFormat : Error + data object Duplicated : Error } } \ No newline at end of file diff --git a/core/domain/src/main/java/com/example/domain/model/Writer.kt b/core/domain/src/main/java/com/example/domain/model/Writer.kt index d501bcc6..1bb7dcb5 100644 --- a/core/domain/src/main/java/com/example/domain/model/Writer.kt +++ b/core/domain/src/main/java/com/example/domain/model/Writer.kt @@ -3,5 +3,5 @@ package com.example.domain.model data class Writer( val userId: Long, val nickname: String, - val profileImg: String, + val profileImg: String?, ) diff --git a/core/domain/src/main/java/com/example/domain/repository/AuthRepository.kt b/core/domain/src/main/java/com/example/domain/repository/AuthRepository.kt index 760143d1..b5ad3284 100644 --- a/core/domain/src/main/java/com/example/domain/repository/AuthRepository.kt +++ b/core/domain/src/main/java/com/example/domain/repository/AuthRepository.kt @@ -1,5 +1,7 @@ package com.example.domain.repository +import com.example.domain.model.NicknameValidationResult + interface AuthRepository { suspend fun kakaoLogin(): Result @@ -7,7 +9,7 @@ interface AuthRepository { kakaoAccessToken: String?, profileImage: String?, nickname: String, - ): Result + ): Result suspend fun logout(): Result diff --git a/core/ui/src/main/java/com/example/ui/controller/BottomNavigationController.kt b/core/ui/src/main/java/com/example/ui/controller/BottomNavigationController.kt new file mode 100644 index 00000000..51a65648 --- /dev/null +++ b/core/ui/src/main/java/com/example/ui/controller/BottomNavigationController.kt @@ -0,0 +1,24 @@ +package com.example.ui.controller + +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue + +class BottomNavigationController { + var bottomNavigationVisible by mutableStateOf(true) + private set + + fun show() { + bottomNavigationVisible = true + } + + fun hide() { + bottomNavigationVisible = false + } +} + +val LocalBottomNavigationController = + compositionLocalOf { + error("BottomNavigationController not provided") + } diff --git a/core/ui/src/main/java/com/example/ui/mapper/NicknameValidationMapper.kt b/core/ui/src/main/java/com/example/ui/mapper/NicknameValidationMapper.kt index 5034ba87..38e61d63 100644 --- a/core/ui/src/main/java/com/example/ui/mapper/NicknameValidationMapper.kt +++ b/core/ui/src/main/java/com/example/ui/mapper/NicknameValidationMapper.kt @@ -9,4 +9,5 @@ fun NicknameValidationResult.toUiState(): InputState = NicknameValidationResult.Success -> NicknameInputState.Success NicknameValidationResult.Error.TooShort -> NicknameInputState.Error.NotEnoughLength NicknameValidationResult.Error.InvalidFormat -> NicknameInputState.Error.InvalidFormat + NicknameValidationResult.Error.Duplicated -> NicknameInputState.Error.AlreadyExists } diff --git a/feature/comment/src/main/java/com/example/comment/CommentContract.kt b/feature/comment/src/main/java/com/example/comment/CommentContract.kt index a2233af5..3fbd3a98 100644 --- a/feature/comment/src/main/java/com/example/comment/CommentContract.kt +++ b/feature/comment/src/main/java/com/example/comment/CommentContract.kt @@ -37,5 +37,9 @@ class CommentContract { data object NavigateToBack : CommentSideEffect data object NavigateToHome : CommentSideEffect + + data class OpenWebView( + val url: String, + ) : CommentSideEffect } } diff --git a/feature/comment/src/main/java/com/example/comment/CommentScreen.kt b/feature/comment/src/main/java/com/example/comment/CommentScreen.kt index f71f915f..dfb5d847 100644 --- a/feature/comment/src/main/java/com/example/comment/CommentScreen.kt +++ b/feature/comment/src/main/java/com/example/comment/CommentScreen.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.layout import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -47,6 +48,8 @@ fun CommentRoute( ) { val state by viewModel.uiState.collectAsStateWithLifecycle() + val uriHandler = LocalUriHandler.current + LaunchedEffect(track) { viewModel.handleIntent(CommentContract.CommentIntent.Initialize(track)) } @@ -60,6 +63,9 @@ fun CommentRoute( CommentContract.CommentSideEffect.NavigateToHome -> { navigator.clearAndNavigateTo(Home) } + is CommentContract.CommentSideEffect.OpenWebView -> { + uriHandler.openUri(sideEffect.url) + } } } } diff --git a/feature/comment/src/main/java/com/example/comment/CommentViewModel.kt b/feature/comment/src/main/java/com/example/comment/CommentViewModel.kt index c10d759c..328412fc 100644 --- a/feature/comment/src/main/java/com/example/comment/CommentViewModel.kt +++ b/feature/comment/src/main/java/com/example/comment/CommentViewModel.kt @@ -1,6 +1,7 @@ package com.example.comment import androidx.lifecycle.viewModelScope +import com.example.common.constant.Url import com.example.domain.repository.PostRepository import com.example.ui.base.BaseViewModel import com.example.ui.model.TrackState @@ -42,7 +43,7 @@ class CommentViewModel } } CommentContract.CommentIntent.OnMoreGuideClick -> { - // 가이드 노션으로 이동 + setSideEffect(CommentContract.CommentSideEffect.OpenWebView(Url.COMMUNITY_GUIDE)) } CommentContract.CommentIntent.OnRegisterButtonClick -> { registerPost() @@ -59,7 +60,7 @@ class CommentViewModel track = track, comment = currentState.commentInput, ).onSuccess { - setSideEffect(CommentContract.CommentSideEffect.NavigateToBack) + setSideEffect(CommentContract.CommentSideEffect.NavigateToHome) }.onFailure { } } diff --git a/feature/detail/src/main/java/com/example/detail/DetailScreen.kt b/feature/detail/src/main/java/com/example/detail/DetailScreen.kt index a098c7e3..6a4b6ccb 100644 --- a/feature/detail/src/main/java/com/example/detail/DetailScreen.kt +++ b/feature/detail/src/main/java/com/example/detail/DetailScreen.kt @@ -239,7 +239,7 @@ private fun DetailScreen( verticalAlignment = Alignment.CenterVertically, ) { AsyncImage( - model = state.writer.profileImg, + model = state.writer.profileImg ?: R.drawable.base_profile_image, contentDescription = null, modifier = Modifier diff --git a/feature/editprofile/src/main/java/com/example/editprofile/EditProfileScreen.kt b/feature/editprofile/src/main/java/com/example/editprofile/EditProfileScreen.kt index e3c5ea30..01aaa9a1 100644 --- a/feature/editprofile/src/main/java/com/example/editprofile/EditProfileScreen.kt +++ b/feature/editprofile/src/main/java/com/example/editprofile/EditProfileScreen.kt @@ -8,7 +8,6 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -17,22 +16,19 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue 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.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import coil3.compose.AsyncImage import com.dplay.designsystem.R import com.example.designsystem.component.DPlayButtonBottomSheet +import com.example.designsystem.component.DPlayProfileImageArea import com.example.designsystem.component.DplayLeftIconTitleTopAppBar import com.example.designsystem.component.button.DPlayCircleButton import com.example.designsystem.component.button.DPlayLargePinkButton @@ -136,36 +132,20 @@ fun EditProfileScreen( Spacer(modifier = Modifier.height(24.dp)) - Box( + DPlayProfileImageArea( + onProfileImageClick = onProfileImageClick, + profileImagePath = state.profileImagePath, modifier = Modifier - .align(Alignment.CenterHorizontally) - .noRippleClickable( - onClick = { onProfileImageClick() }, - ), + .size(116.dp) + .align(Alignment.CenterHorizontally), ) { - AsyncImage( - model = state.profileImagePath ?: R.drawable.img_profile, - contentDescription = null, - modifier = - Modifier - .size(116.dp) - .clip(CircleShape) - .border( - width = 1.dp, - color = DPlayTheme.colors.gray200, - shape = CircleShape, - ), - contentScale = ContentScale.Crop, - ) - DPlayCircleButton( circleButtonType = CircleButtonType.SmallPlus( R.string.add_profile_image_button_icon_description, ), onClick = { onProfileImageClick() }, - modifier = Modifier.align(Alignment.BottomEnd), ) } diff --git a/feature/login/src/main/java/com/example/login/LoginScreen.kt b/feature/login/src/main/java/com/example/login/LoginScreen.kt index ed430271..b2e3fedc 100644 --- a/feature/login/src/main/java/com/example/login/LoginScreen.kt +++ b/feature/login/src/main/java/com/example/login/LoginScreen.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.Text @@ -68,6 +69,8 @@ fun LoginScreen( style = DPlayTheme.typography.bodyBold16, ) + Spacer(modifier = Modifier.height(8.dp)) + Image( painter = painterResource(R.drawable.img_wordmark_pink), contentDescription = null, diff --git a/feature/main/src/main/java/com/example/main/BottomNavigationBar.kt b/feature/main/src/main/java/com/example/main/BottomNavigationBar.kt index 3a8cbf05..ceab4a23 100644 --- a/feature/main/src/main/java/com/example/main/BottomNavigationBar.kt +++ b/feature/main/src/main/java/com/example/main/BottomNavigationBar.kt @@ -41,7 +41,9 @@ fun BottomNavigationBar( ) { if (isVisible) { Box( - modifier = modifier.fillMaxWidth(), + modifier = + modifier + .fillMaxWidth(), ) { Row( modifier = diff --git a/feature/main/src/main/java/com/example/main/MainActivity.kt b/feature/main/src/main/java/com/example/main/MainActivity.kt index 028bd8b3..c4dc44a9 100644 --- a/feature/main/src/main/java/com/example/main/MainActivity.kt +++ b/feature/main/src/main/java/com/example/main/MainActivity.kt @@ -5,6 +5,9 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.BackHandler import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.compose.animation.ContentTransform +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize @@ -32,6 +35,8 @@ import com.example.designsystem.component.snackbar.type.SnackBarType import com.example.designsystem.theme.DPlayTheme import com.example.navigation.Navigator import com.example.navigation.Search +import com.example.ui.controller.BottomNavigationController +import com.example.ui.controller.LocalBottomNavigationController import com.example.ui.controller.LocalModalController import com.example.ui.controller.ModalController import com.example.ui.handler.AppTerminationHandler @@ -52,12 +57,14 @@ class MainActivity : ComponentActivity() { enableEdgeToEdge() setContent { val modalController = remember { ModalController() } + val bottomNavigationController = remember { BottomNavigationController() } val appTerminationHandler = remember(this) { AppTerminationHandler(this) } var snackBarType by remember { mutableStateOf(null) } var snackBarAction by remember { mutableStateOf<(() -> Unit)?>(null) } CompositionLocalProvider( LocalModalController provides modalController, + LocalBottomNavigationController provides bottomNavigationController, LocalSnackBarState provides snackBarType, LocalShowSnackBar provides { type, action -> snackBarType = type @@ -77,7 +84,7 @@ class MainActivity : ComponentActivity() { Modifier.navigationBarsPadding(), bottomBar = { BottomNavigationBar( - isVisible = navigator.shouldShowBottomSheet, + isVisible = navigator.shouldShowBottomSheet && bottomNavigationController.bottomNavigationVisible, topLevelRouteList = navigator.topLevelRoutes, currentTab = navigator.currentScreen, onBottomNavigationItemClick = { route -> @@ -89,8 +96,13 @@ class MainActivity : ComponentActivity() { ) }, ) { padding -> + val bottomPadding = if (navigator.shouldShowBottomSheet) padding.calculateBottomPadding() else 0.dp NavDisplay( - modifier = Modifier.fillMaxSize().background(color = DPlayTheme.colors.dplayWhite).padding(bottom = padding.calculateBottomPadding()), + modifier = + Modifier + .fillMaxSize() + .background(color = DPlayTheme.colors.dplayWhite) + .padding(bottom = bottomPadding), backStack = navigator.backStack, onBack = { navigator.navigateToBack() @@ -100,6 +112,18 @@ class MainActivity : ComponentActivity() { rememberSaveableStateHolderNavEntryDecorator(), rememberViewModelStoreNavEntryDecorator(), ), + transitionSpec = { + ContentTransform( + targetContentEnter = EnterTransition.None, + initialContentExit = ExitTransition.None, + ) + }, + popTransitionSpec = { + ContentTransform( + targetContentEnter = EnterTransition.None, + initialContentExit = ExitTransition.None, + ) + }, entryProvider = entryProvider { entryProviders.forEach { installer -> diff --git a/feature/mypage/src/main/java/com/example/mypage/MyPageContract.kt b/feature/mypage/src/main/java/com/example/mypage/MyPageContract.kt index f8a5d494..0f6d14cd 100644 --- a/feature/mypage/src/main/java/com/example/mypage/MyPageContract.kt +++ b/feature/mypage/src/main/java/com/example/mypage/MyPageContract.kt @@ -1,6 +1,8 @@ package com.example.mypage import com.example.ui.base.BaseContract +import kotlinx.collections.immutable.PersistentSet +import kotlinx.collections.immutable.persistentSetOf class MyPageContract { data class MyPageState( @@ -9,6 +11,9 @@ class MyPageContract { val profileImagePath: String? = null, val selectedTabIndex: Int = 0, val registeredMusicCount: Int = -1, + val isDeleteBottomSheetVisible: Boolean = false, + val selectedPostId: Long = -1, + val deletedTrackIds: PersistentSet = persistentSetOf(), ) : BaseContract.State sealed interface MyPageIntent : BaseContract.Intent { @@ -20,21 +25,23 @@ class MyPageContract { val tabIndex: Int, ) : MyPageIntent - data class OnMusicItemClick( - val musicId: Long, + data class OnScrappedTrackClick( + val postId: Long, ) : MyPageIntent data class OnKebabIconClick( val musicId: Long, ) : MyPageIntent + data class OnRegisteredTrackClick( + val postId: Long, + ) : MyPageIntent + data object OnBottomSheetDeleteClick : MyPageIntent data object OnBottomSheetCancelClick : MyPageIntent data object OnDialogDeleteClick : MyPageIntent - - data object OnDialogCancelClick : MyPageIntent } sealed interface MyPageSideEffect : BaseContract.SideEffect { @@ -43,11 +50,13 @@ class MyPageContract { data object NavigateToEditProfile : MyPageSideEffect data class NavigateToDetail( - val musicId: Long, + val postId: Long, ) : MyPageSideEffect - data object ShowDeleteBottomSheet : MyPageSideEffect - data object ShowDeleteDialogue : MyPageSideEffect + + data object HideBottomNavigation : MyPageSideEffect + + data object ShowBottomNavigation : MyPageSideEffect } } diff --git a/feature/mypage/src/main/java/com/example/mypage/MyPageScreen.kt b/feature/mypage/src/main/java/com/example/mypage/MyPageScreen.kt index 9b237814..948d4a38 100644 --- a/feature/mypage/src/main/java/com/example/mypage/MyPageScreen.kt +++ b/feature/mypage/src/main/java/com/example/mypage/MyPageScreen.kt @@ -1,8 +1,10 @@ package com.example.mypage +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically 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.BoxWithConstraints @@ -20,7 +22,6 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -31,8 +32,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue 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.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle @@ -46,18 +46,22 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.itemKey -import coil3.compose.AsyncImage import com.dplay.designsystem.R +import com.example.designsystem.component.DPlayButtonBottomSheet import com.example.designsystem.component.DPlayMusicGridItem import com.example.designsystem.component.DPlayMusicListItem +import com.example.designsystem.component.DPlayProfileImageArea import com.example.designsystem.component.DplayRightIconTitleTopAppBar import com.example.designsystem.component.button.DPlayCircleButton import com.example.designsystem.component.button.type.CircleButtonType import com.example.designsystem.theme.DPlayTheme import com.example.designsystem.util.noRippleClickable +import com.example.navigation.Detail import com.example.navigation.EditProfile import com.example.navigation.Navigator import com.example.navigation.Setting +import com.example.ui.controller.LocalBottomNavigationController +import com.example.ui.controller.LocalModalController import com.example.ui.emptyLazyPagingItems import com.example.ui.model.RegisteredTrackState import com.example.ui.model.ScrappedTrackState @@ -74,18 +78,42 @@ fun MyPageRoute( val registeredTracks = viewModel.registeredTracks.collectAsLazyPagingItems() val scrappedTracks = viewModel.scrappedTracks.collectAsLazyPagingItems() + val context = LocalContext.current + val bottomNavigationController = LocalBottomNavigationController.current + val modalController = LocalModalController.current + LaunchedEffect(Unit) { viewModel.sideEffect.collectLatest { sideEffect -> when (sideEffect) { - is MyPageContract.MyPageSideEffect.NavigateToDetail -> TODO() + is MyPageContract.MyPageSideEffect.NavigateToDetail -> { + navigator.navigateTo(destination = Detail(postId = sideEffect.postId)) + } MyPageContract.MyPageSideEffect.NavigateToEditProfile -> { navigator.navigateTo(destination = EditProfile) } MyPageContract.MyPageSideEffect.NavigateToSettings -> { navigator.navigateTo(destination = Setting) } - MyPageContract.MyPageSideEffect.ShowDeleteBottomSheet -> TODO() - MyPageContract.MyPageSideEffect.ShowDeleteDialogue -> TODO() + MyPageContract.MyPageSideEffect.HideBottomNavigation -> { + bottomNavigationController.hide() + } + MyPageContract.MyPageSideEffect.ShowBottomNavigation -> { + bottomNavigationController.show() + } + MyPageContract.MyPageSideEffect.ShowDeleteDialogue -> { + modalController.showWarningModal( + mainText = "정말 삭제하시겠어요?", + subText = null, + onLeftButtonClick = { modalController.hideModal() }, + onRightButtonClick = { + modalController.hideModal() + viewModel.handleIntent(MyPageContract.MyPageIntent.OnDialogDeleteClick) + }, + onDismiss = { modalController.hideModal() }, + leftButtonLabel = "취소", + rightButtonLabel = "삭제하기", + ) + } } } } @@ -104,6 +132,21 @@ fun MyPageRoute( onProfileImageClick = { viewModel.handleIntent(MyPageContract.MyPageIntent.OnProfileClick) }, + onScrappedTrackClick = { + viewModel.handleIntent(MyPageContract.MyPageIntent.OnScrappedTrackClick(it)) + }, + onKebabIconClick = { + viewModel.handleIntent(MyPageContract.MyPageIntent.OnKebabIconClick(it)) + }, + onBottomSheetCancelClick = { + viewModel.handleIntent(MyPageContract.MyPageIntent.OnBottomSheetCancelClick) + }, + onBottomSheetDeleteClick = { + viewModel.handleIntent(MyPageContract.MyPageIntent.OnBottomSheetDeleteClick) + }, + onRegisteredTrackClick = { + viewModel.handleIntent(MyPageContract.MyPageIntent.OnRegisteredTrackClick(it)) + }, ) } @@ -112,40 +155,83 @@ fun MyPageScreen( state: MyPageContract.MyPageState, registeredTrackList: LazyPagingItems, scrappedTrackList: LazyPagingItems, + modifier: Modifier = Modifier, onTabSelected: (Int) -> Unit = {}, onSettingIconClick: () -> Unit = {}, onProfileImageClick: () -> Unit = {}, - modifier: Modifier = Modifier, + onScrappedTrackClick: (Long) -> Unit = {}, + onKebabIconClick: (Long) -> Unit = {}, + onBottomSheetCancelClick: () -> Unit = {}, + onBottomSheetDeleteClick: () -> Unit = {}, + onRegisteredTrackClick: (Long) -> Unit = {}, ) { - Column( - modifier = - modifier - .fillMaxSize() - .background(DPlayTheme.colors.dplayWhite), + Box( + modifier = Modifier.fillMaxSize(), ) { - DplayRightIconTitleTopAppBar( - title = stringResource(com.dplay.mypage.R.string.mypage_screen_title), + Column( + modifier = + modifier + .fillMaxSize() + .background(DPlayTheme.colors.dplayWhite), ) { - onSettingIconClick() - } + DplayRightIconTitleTopAppBar( + title = stringResource(com.dplay.mypage.R.string.mypage_screen_title), + ) { + onSettingIconClick() + } - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(12.dp)) - UserInformationRow( - nickname = state.userNickname, - registeredMusicCount = state.registeredMusicCount, - profileImagePath = state.profileImagePath, - onProfileImageClick = { onProfileImageClick() }, - ) + UserInformationRow( + nickname = state.userNickname, + registeredMusicCount = state.registeredMusicCount, + profileImagePath = state.profileImagePath, + onProfileImageClick = { onProfileImageClick() }, + ) - Spacer(modifier = Modifier.height(20.dp)) + Spacer(modifier = Modifier.height(20.dp)) - TabContent( - selectedTabIndex = state.selectedTabIndex, - onTabSelected = onTabSelected, - registeredTrackList = registeredTrackList, - scrappedTrackList = scrappedTrackList, - ) + TabContent( + selectedTabIndex = state.selectedTabIndex, + onTabSelected = onTabSelected, + registeredTrackList = registeredTrackList, + scrappedTrackList = scrappedTrackList, + onScrappedTrackClick = onScrappedTrackClick, + onKebabIconClick = onKebabIconClick, + onRegisteredTrackClick = onRegisteredTrackClick, + ) + } + if (state.isDeleteBottomSheetVisible) { + Box( + modifier = + Modifier + .fillMaxSize() + .background(color = DPlayTheme.colors.dim40) + .noRippleClickable { onBottomSheetCancelClick() }, + ) + } + + AnimatedVisibility( + visible = state.isDeleteBottomSheetVisible, + modifier = Modifier.align(Alignment.BottomCenter), + enter = + slideInVertically( + initialOffsetY = { it }, + ), + exit = + slideOutVertically( + targetOffsetY = { 0 }, + ), + ) { + DPlayButtonBottomSheet( + mainText = "삭제하기", + subText = "취소하기", + mainOnClick = { onBottomSheetDeleteClick() }, + subOnClick = { onBottomSheetCancelClick() }, + modifier = Modifier.noRippleClickable(), + mainButtonColor = DPlayTheme.colors.alertRed, + ) + } } } @@ -193,35 +279,17 @@ private fun UserInformationRow( ) } - Box( - modifier = - Modifier - .noRippleClickable( - onClick = { onProfileImageClick() }, - ), + DPlayProfileImageArea( + onProfileImageClick = onProfileImageClick, + profileImagePath = profileImagePath, + modifier = Modifier.size(80.dp), ) { - AsyncImage( - model = profileImagePath ?: R.drawable.img_profile, - contentDescription = null, - modifier = - Modifier - .size(80.dp) - .clip(CircleShape) - .border( - width = 1.dp, - color = DPlayTheme.colors.gray200, - shape = CircleShape, - ), - contentScale = ContentScale.Crop, - ) - DPlayCircleButton( circleButtonType = CircleButtonType.SmallEdit( R.string.edit_profile_image_button_icon_description, ), onClick = {}, - modifier = Modifier.align(Alignment.BottomEnd), ) } } @@ -233,6 +301,9 @@ private fun TabContent( registeredTrackList: LazyPagingItems, scrappedTrackList: LazyPagingItems, onTabSelected: (Int) -> Unit, + onScrappedTrackClick: (Long) -> Unit = {}, + onKebabIconClick: (Long) -> Unit = {}, + onRegisteredTrackClick: (Long) -> Unit = {}, ) { Column { MyPageTabRow( @@ -250,10 +321,13 @@ private fun TabContent( 0 -> RegisteredMusicList( registeredTrackList = registeredTrackList, + onKebabIconClick = onKebabIconClick, + onRegisteredTrackClick = onRegisteredTrackClick, ) 1 -> BookmarkedMusicList( scrappedTrackList = scrappedTrackList, + onScrappedTrackClick = onScrappedTrackClick, ) } } @@ -263,8 +337,8 @@ private fun TabContent( @Composable private fun MyPageTabRow( selectedTabIndex: Int, - onTabSelected: (Int) -> Unit = {}, modifier: Modifier = Modifier, + onTabSelected: (Int) -> Unit = {}, ) { val tabs = listOf( @@ -331,6 +405,15 @@ private fun MyPageTabRow( .height(2.dp) .background(color = DPlayTheme.colors.dplayBlack), ) + + Box( + modifier = + Modifier + .fillMaxWidth() + .height(1.dp) + .background(color = DPlayTheme.colors.gray200) + .align(Alignment.BottomCenter), + ) } } } @@ -340,26 +423,34 @@ private fun MyPageTabRow( private fun RegisteredMusicList( registeredTrackList: LazyPagingItems, modifier: Modifier = Modifier, + onKebabIconClick: (Long) -> Unit = {}, + onRegisteredTrackClick: (Long) -> Unit = {}, ) { - LazyColumn( - contentPadding = PaddingValues(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), - ) { - items( - count = registeredTrackList.itemCount, - key = registeredTrackList.itemKey { it.postId }, - ) { index -> - val registeredTrack = registeredTrackList[index] - - if (registeredTrack != null) { - DPlayMusicListItem( - musicImageUrl = registeredTrack.track.thumbnailUrl, - musicName = registeredTrack.track.musicTitle, - musicArtistName = registeredTrack.track.artistName, - musicContent = registeredTrack.comment, - onMoreClick = {}, - onClick = {}, - ) + if (registeredTrackList.itemCount == 0) { + RegisteredMusicEmptyView() + } else { + Spacer(modifier = Modifier.height(12.dp)) + + LazyColumn( + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + items( + count = registeredTrackList.itemCount, + key = registeredTrackList.itemKey { it.postId }, + ) { index -> + val registeredTrack = registeredTrackList[index] + + if (registeredTrack != null) { + DPlayMusicListItem( + musicImageUrl = registeredTrack.track.thumbnailUrl, + musicName = registeredTrack.track.musicTitle, + musicArtistName = registeredTrack.track.artistName, + musicContent = registeredTrack.comment, + onMoreClick = { onKebabIconClick(registeredTrack.postId) }, + onClick = { onRegisteredTrackClick(registeredTrack.postId) }, + ) + } } } } @@ -369,33 +460,70 @@ private fun RegisteredMusicList( private fun BookmarkedMusicList( scrappedTrackList: LazyPagingItems, modifier: Modifier = Modifier, + onScrappedTrackClick: (Long) -> Unit = {}, ) { - LazyVerticalGrid( - modifier = modifier, - columns = GridCells.Fixed(3), - contentPadding = PaddingValues(16.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), - ) { - items( - count = scrappedTrackList.itemCount, - key = scrappedTrackList.itemKey { it.postId }, - ) { index -> - val scrappedTrack = scrappedTrackList[index] - - if (scrappedTrack != null) { - DPlayMusicGridItem( - musicImageUrl = scrappedTrack.track.thumbnailUrl, - musicName = scrappedTrack.track.musicTitle, - musicArtistName = scrappedTrack.track.artistName, - onClick = {}, - ) - } else { + if (scrappedTrackList.itemCount == 0) { + ScrappedMusicEmptyView() + } else { + LazyVerticalGrid( + modifier = modifier, + columns = GridCells.Fixed(3), + contentPadding = PaddingValues(16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + items( + count = scrappedTrackList.itemCount, + key = scrappedTrackList.itemKey { it.postId }, + ) { index -> + val scrappedTrack = scrappedTrackList[index] + + if (scrappedTrack != null) { + DPlayMusicGridItem( + musicImageUrl = scrappedTrack.track.thumbnailUrl, + musicName = scrappedTrack.track.musicTitle, + musicArtistName = scrappedTrack.track.artistName, + onClick = { onScrappedTrackClick(scrappedTrack.postId) }, + ) + } else { + } } } } } +@Composable +private fun RegisteredMusicEmptyView() { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.height(184.dp)) + + Text( + text = "아직 등록한 곡이 없어요", + style = DPlayTheme.typography.bodySemi14, + color = DPlayTheme.colors.gray400, + ) + } +} + +@Composable +private fun ScrappedMusicEmptyView() { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.height(184.dp)) + + Text( + text = "아직 등록한 곡이 없어요", + style = DPlayTheme.typography.bodySemi14, + color = DPlayTheme.colors.gray400, + ) + } +} + @Preview @Composable private fun MyPageScreenPreview() { diff --git a/feature/mypage/src/main/java/com/example/mypage/MyPageViewModel.kt b/feature/mypage/src/main/java/com/example/mypage/MyPageViewModel.kt index 91411798..c085867f 100644 --- a/feature/mypage/src/main/java/com/example/mypage/MyPageViewModel.kt +++ b/feature/mypage/src/main/java/com/example/mypage/MyPageViewModel.kt @@ -3,7 +3,9 @@ package com.example.mypage import androidx.lifecycle.viewModelScope import androidx.paging.PagingData import androidx.paging.cachedIn +import androidx.paging.filter import androidx.paging.map +import com.example.domain.repository.PostRepository import com.example.domain.repository.UserRepository import com.example.domain.usecase.GetMyRegisteredTracksUseCase import com.example.domain.usecase.GetMyScrappedTracksUseCase @@ -13,9 +15,11 @@ import com.example.ui.model.ScrappedTrackState import com.example.ui.model.toUiState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -24,6 +28,7 @@ class MyPageViewModel @Inject constructor( private val userRepository: UserRepository, + private val postRepository: PostRepository, private val getMyRegisteredTracksUseCase: GetMyRegisteredTracksUseCase, private val getMyScrappedTracksUseCase: GetMyScrappedTracksUseCase, ) : BaseViewModel( @@ -45,6 +50,9 @@ class MyPageViewModel registeredTrack.toUiState() } }.cachedIn(viewModelScope) + .combine(uiState) { pagingData, state -> + pagingData.filter { !state.deletedTrackIds.contains(it.postId) } + } val scrappedTracks: Flow> = getMyScrappedTracksUseCase() @@ -57,24 +65,57 @@ class MyPageViewModel override fun handleIntent(intent: MyPageContract.MyPageIntent) { when (intent) { MyPageContract.MyPageIntent.OnBottomSheetCancelClick -> { + updateState { + copy( + isDeleteBottomSheetVisible = false, + ) + } + setSideEffect(MyPageContract.MyPageSideEffect.ShowBottomNavigation) } MyPageContract.MyPageIntent.OnBottomSheetDeleteClick -> { + setSideEffect(MyPageContract.MyPageSideEffect.ShowBottomNavigation) + updateState { + copy( + isDeleteBottomSheetVisible = false, + ) + } + setSideEffect(MyPageContract.MyPageSideEffect.ShowDeleteDialogue) } - MyPageContract.MyPageIntent.OnDialogCancelClick -> { + is MyPageContract.MyPageIntent.OnRegisteredTrackClick -> { + setSideEffect(MyPageContract.MyPageSideEffect.NavigateToDetail(intent.postId)) } MyPageContract.MyPageIntent.OnDialogDeleteClick -> { - // 삭제 api + viewModelScope.launch { + val selectedPostId = currentState.selectedPostId + postRepository + .deletePost(selectedPostId) + .onSuccess { + updateState { + copy( + deletedTrackIds = deletedTrackIds.add(selectedPostId), + ) + } + }.onFailure { + Timber.d(t = it, message = "deletePost 실패") + } + } } is MyPageContract.MyPageIntent.OnKebabIconClick -> { - setSideEffect(MyPageContract.MyPageSideEffect.ShowDeleteBottomSheet) + setSideEffect(MyPageContract.MyPageSideEffect.HideBottomNavigation) + updateState { + copy( + isDeleteBottomSheetVisible = true, + selectedPostId = intent.musicId, + ) + } } - is MyPageContract.MyPageIntent.OnMusicItemClick -> { - setSideEffect(MyPageContract.MyPageSideEffect.NavigateToDetail(intent.musicId)) + is MyPageContract.MyPageIntent.OnScrappedTrackClick -> { + setSideEffect(MyPageContract.MyPageSideEffect.NavigateToDetail(intent.postId)) } MyPageContract.MyPageIntent.OnProfileClick -> { diff --git a/feature/onboarding/src/main/java/com/example/onboarding/OnboardingContract.kt b/feature/onboarding/src/main/java/com/example/onboarding/OnboardingContract.kt index 1daf96fe..77745ff4 100644 --- a/feature/onboarding/src/main/java/com/example/onboarding/OnboardingContract.kt +++ b/feature/onboarding/src/main/java/com/example/onboarding/OnboardingContract.kt @@ -10,7 +10,7 @@ class OnboardingContract { val kakaoAccessToken: String? = null, val agreedTerms: Set = emptySet(), val nickname: String = "", - val profileImageUri: Uri? = null, + val profileImagePath: String? = null, val nicknameInputState: InputState = InputState.Default, val isAlbumLauncherBottomSheetVisible: Boolean = false, ) : BaseContract.State { @@ -39,6 +39,10 @@ class OnboardingContract { data object OnTermsScreenNextButtonClick : OnboardingIntent + data class OnTermsArrowClick( + val term: TermType, + ) : OnboardingIntent + data class OnNicknameChanged( val input: String, ) : OnboardingIntent @@ -59,6 +63,8 @@ class OnboardingContract { data object OnStartButtonClick : OnboardingIntent + data object OnBackGestureAfterSignup : OnboardingIntent + data object OnPermissionConfirmButtonClick : OnboardingIntent data class OnNotificationPermissionResult( @@ -77,8 +83,14 @@ class OnboardingContract { data object NavigateToHome : OnboardingSideEffect + data object NavigateToLogin : OnboardingSideEffect + data object ShowPermissionDialog : OnboardingSideEffect data object LaunchAlbum : OnboardingSideEffect + + data class OpenWebView( + val url: String, + ) : OnboardingSideEffect } } diff --git a/feature/onboarding/src/main/java/com/example/onboarding/OnboardingNavDisplay.kt b/feature/onboarding/src/main/java/com/example/onboarding/OnboardingNavDisplay.kt index 36bd80ea..9e4e168e 100644 --- a/feature/onboarding/src/main/java/com/example/onboarding/OnboardingNavDisplay.kt +++ b/feature/onboarding/src/main/java/com/example/onboarding/OnboardingNavDisplay.kt @@ -1,5 +1,8 @@ package com.example.onboarding +import androidx.compose.animation.ContentTransform +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember @@ -30,6 +33,18 @@ fun OnboardingNavDisplay( globalNavigator.navigateToBack() } }, + transitionSpec = { + ContentTransform( + targetContentEnter = EnterTransition.None, + initialContentExit = ExitTransition.None, + ) + }, + popTransitionSpec = { + ContentTransform( + targetContentEnter = EnterTransition.None, + initialContentExit = ExitTransition.None, + ) + }, entryProvider = entryProvider { // 1. 약관 동의 화면 @@ -51,6 +66,7 @@ fun OnboardingNavDisplay( entry { OnboardingRoute( onboardingNavigator = onboardingNavigator, + globalNavigator = globalNavigator, ) } diff --git a/feature/onboarding/src/main/java/com/example/onboarding/OnboardingProfileScreen.kt b/feature/onboarding/src/main/java/com/example/onboarding/OnboardingProfileScreen.kt index 423e51a3..e52be3f5 100644 --- a/feature/onboarding/src/main/java/com/example/onboarding/OnboardingProfileScreen.kt +++ b/feature/onboarding/src/main/java/com/example/onboarding/OnboardingProfileScreen.kt @@ -8,7 +8,6 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -17,23 +16,20 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue 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.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import coil3.compose.AsyncImage import com.dplay.designsystem.R import com.example.designsystem.component.DPlayButtonBottomSheet +import com.example.designsystem.component.DPlayProfileImageArea import com.example.designsystem.component.DplayLeftIconTopAppBar import com.example.designsystem.component.button.DPlayCircleButton import com.example.designsystem.component.button.DPlayLargePinkButton @@ -137,6 +133,8 @@ fun OnboardingProfileScreen( ) { DplayLeftIconTopAppBar { onBackButtonClick() } + Spacer(Modifier.height(20.dp)) + Text( text = stringResource(com.dplay.onboarding.R.string.profile_screen_title), modifier = Modifier.padding(start = 16.dp), @@ -146,36 +144,20 @@ fun OnboardingProfileScreen( Spacer(modifier = Modifier.height(28.dp)) - Box( + DPlayProfileImageArea( + onProfileImageClick = onProfileImageClick, + profileImagePath = state.profileImagePath, modifier = Modifier - .align(Alignment.CenterHorizontally) - .noRippleClickable( - onClick = { onProfileImageClick() }, - ), + .size(116.dp) + .align(Alignment.CenterHorizontally), ) { - AsyncImage( - model = state.profileImageUri ?: R.drawable.img_profile, - contentDescription = null, - modifier = - Modifier - .size(116.dp) - .clip(CircleShape) - .border( - width = 1.dp, - color = DPlayTheme.colors.gray200, - shape = CircleShape, - ), - contentScale = ContentScale.Crop, - ) - DPlayCircleButton( circleButtonType = CircleButtonType.SmallPlus( R.string.add_profile_image_button_icon_description, ), onClick = { onProfileImageClick() }, - modifier = Modifier.align(Alignment.BottomEnd), ) } @@ -195,7 +177,7 @@ fun OnboardingProfileScreen( DPlayLargePinkButton( onClick = { onNextButtonClick() }, - label = stringResource(R.string.next_button_label), + label = stringResource(R.string.enroll_button_label), modifier = Modifier .padding(horizontal = 16.dp) diff --git a/feature/onboarding/src/main/java/com/example/onboarding/OnboardingScreen.kt b/feature/onboarding/src/main/java/com/example/onboarding/OnboardingScreen.kt index edf17278..86294c58 100644 --- a/feature/onboarding/src/main/java/com/example/onboarding/OnboardingScreen.kt +++ b/feature/onboarding/src/main/java/com/example/onboarding/OnboardingScreen.kt @@ -1,5 +1,6 @@ package com.example.onboarding +import androidx.activity.compose.BackHandler import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -28,15 +29,18 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import com.dplay.designsystem.R -import com.example.designsystem.component.DplayLeftIconTopAppBar +import com.dplay.onboarding.R.drawable +import com.example.designsystem.component.DplayTopAppBar import com.example.designsystem.component.button.DPlayLargePinkButton import com.example.designsystem.theme.DPlayTheme +import com.example.navigation.Login import com.example.navigation.Navigator import com.example.navigation.OnboardingGraph import kotlinx.coroutines.flow.collectLatest @Composable fun OnboardingRoute( + globalNavigator: Navigator, onboardingNavigator: Navigator, modifier: Modifier = Modifier, viewModel: OnboardingViewModel = hiltViewModel(), @@ -50,6 +54,9 @@ fun OnboardingRoute( OnboardingContract.OnboardingSideEffect.NavigateToPermission -> { onboardingNavigator.navigateTo(OnboardingGraph.Permission) } + OnboardingContract.OnboardingSideEffect.NavigateToLogin -> { + globalNavigator.clearAndNavigateTo(Login) + } else -> {} } } @@ -59,8 +66,8 @@ fun OnboardingRoute( onStartButtonClick = { viewModel.handleIntent(OnboardingContract.OnboardingIntent.OnStartButtonClick) }, - onBackButtonClick = { - viewModel.handleIntent(OnboardingContract.OnboardingIntent.OnBackButtonClick) + onBackGesture = { + viewModel.handleIntent(OnboardingContract.OnboardingIntent.OnBackGestureAfterSignup) }, ) } @@ -69,8 +76,13 @@ fun OnboardingRoute( fun OnboardingScreen( modifier: Modifier = Modifier, onStartButtonClick: () -> Unit = {}, - onBackButtonClick: () -> Unit = {}, + onBackGesture: () -> Unit = {}, ) { + BackHandler( + enabled = true, + onBack = onBackGesture, + ) + val pagerState = rememberPagerState(pageCount = { 3 }) Column( @@ -80,9 +92,7 @@ fun OnboardingScreen( .background(color = DPlayTheme.colors.dplayWhite) .padding(bottom = 16.dp), ) { - DplayLeftIconTopAppBar { - onBackButtonClick() - } + DplayTopAppBar { } Spacer(modifier = Modifier.height(40.dp)) @@ -145,6 +155,14 @@ private fun FirstOnboardingPage( color = DPlayTheme.colors.gray400, textAlign = TextAlign.Center, ) + + Spacer(modifier = Modifier.height(12.dp)) + + Image( + painter = painterResource(id = drawable.img_onboarding_1), + contentDescription = null, + modifier = Modifier.fillMaxWidth(), + ) } } @@ -171,6 +189,13 @@ private fun SecondOnboardingPage(modifier: Modifier = Modifier) { color = DPlayTheme.colors.gray400, textAlign = TextAlign.Center, ) + + Spacer(modifier = Modifier.height(12.dp)) + + Image( + painter = painterResource(id = drawable.img_onboarding_2), + contentDescription = null, + ) } } @@ -201,7 +226,7 @@ private fun ThirdOnboardingPage(modifier: Modifier = Modifier) { Spacer(modifier = Modifier.height(12.dp)) Image( - painter = painterResource(id = R.drawable.img_onboarding_3), + painter = painterResource(id = drawable.img_onboarding_3), contentDescription = null, ) } diff --git a/feature/onboarding/src/main/java/com/example/onboarding/OnboardingTermsScreen.kt b/feature/onboarding/src/main/java/com/example/onboarding/OnboardingTermsScreen.kt index 9c3be50e..5e8f0109 100644 --- a/feature/onboarding/src/main/java/com/example/onboarding/OnboardingTermsScreen.kt +++ b/feature/onboarding/src/main/java/com/example/onboarding/OnboardingTermsScreen.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -35,6 +36,7 @@ fun OnboardingTermsRoute( modifier: Modifier = Modifier, viewModel: OnboardingViewModel = hiltViewModel(), ) { + val uriHandler = LocalUriHandler.current val state by viewModel.uiState.collectAsStateWithLifecycle() LaunchedEffect(Unit) { @@ -46,6 +48,9 @@ fun OnboardingTermsRoute( OnboardingContract.OnboardingSideEffect.NavigateToProfile -> { onboardingNavigator.navigateTo(OnboardingGraph.Profile) } + is OnboardingContract.OnboardingSideEffect.OpenWebView -> { + uriHandler.openUri(sideEffect.url) + } else -> {} } } @@ -65,6 +70,9 @@ fun OnboardingTermsRoute( onBackButtonClick = { viewModel.handleIntent(OnboardingContract.OnboardingIntent.OnBackButtonClick) }, + onTermsArrowClick = { + viewModel.handleIntent(OnboardingContract.OnboardingIntent.OnTermsArrowClick(it)) + }, ) } @@ -73,6 +81,7 @@ fun OnboardingTermsScreen( state: OnboardingContract.OnboardingState, modifier: Modifier = Modifier, onToggleTerm: (TermType) -> Unit = {}, + onTermsArrowClick: (TermType) -> Unit = {}, onToggleAllTerms: () -> Unit = {}, onNextButtonClick: () -> Unit = {}, onBackButtonClick: () -> Unit = {}, @@ -110,7 +119,7 @@ fun OnboardingTermsScreen( DPlayCheckArrow( text = stringResource(id = R.string.terms_service_required), isChecked = state.agreedTerms.contains(TermType.TERMS_OF_SERVICE), - onArrowClick = {}, + onArrowClick = { onTermsArrowClick(TermType.TERMS_OF_SERVICE) }, onCheckBoxClick = { onToggleTerm(TermType.TERMS_OF_SERVICE) }, modifier = Modifier.fillMaxWidth(), ) @@ -118,7 +127,7 @@ fun OnboardingTermsScreen( DPlayCheckArrow( text = stringResource(id = R.string.privacy_policy_required), isChecked = state.agreedTerms.contains(TermType.PRIVACY_POLICY), - onArrowClick = {}, + onArrowClick = { onTermsArrowClick(TermType.PRIVACY_POLICY) }, onCheckBoxClick = { onToggleTerm(TermType.PRIVACY_POLICY) }, modifier = Modifier.fillMaxWidth(), ) diff --git a/feature/onboarding/src/main/java/com/example/onboarding/OnboardingViewModel.kt b/feature/onboarding/src/main/java/com/example/onboarding/OnboardingViewModel.kt index 364afd87..fc093640 100644 --- a/feature/onboarding/src/main/java/com/example/onboarding/OnboardingViewModel.kt +++ b/feature/onboarding/src/main/java/com/example/onboarding/OnboardingViewModel.kt @@ -2,7 +2,9 @@ package com.example.onboarding import androidx.lifecycle.viewModelScope import com.example.common.type.TermType +import com.example.domain.model.NicknameValidationResult import com.example.domain.repository.AuthRepository +import com.example.domain.repository.UserRepository import com.example.domain.usecase.ValidateNicknameUseCase import com.example.ui.base.BaseViewModel import com.example.ui.mapper.toUiState @@ -16,6 +18,7 @@ class OnboardingViewModel constructor( private val validateNicknameUseCase: ValidateNicknameUseCase, private val authRepository: AuthRepository, + private val userRepository: UserRepository, ) : BaseViewModel( OnboardingContract.OnboardingState(), ) { @@ -41,6 +44,10 @@ class OnboardingViewModel toggleAllTerms() } + is OnboardingContract.OnboardingIntent.OnTermsArrowClick -> { + setSideEffect(OnboardingContract.OnboardingSideEffect.OpenWebView(intent.term.url)) + } + OnboardingContract.OnboardingIntent.OnTermsScreenNextButtonClick -> { setSideEffect(OnboardingContract.OnboardingSideEffect.NavigateToProfile) } @@ -56,7 +63,7 @@ class OnboardingViewModel is OnboardingContract.OnboardingIntent.OnAlbumImageSelect -> { updateState { copy( - profileImageUri = intent.uri, + profileImagePath = intent.uri.toString(), isAlbumLauncherBottomSheetVisible = false, ) } @@ -69,7 +76,7 @@ class OnboardingViewModel OnboardingContract.OnboardingIntent.OnDefaultImageSelect -> { updateState { copy( - profileImageUri = null, + profileImagePath = null, isAlbumLauncherBottomSheetVisible = false, ) } @@ -84,6 +91,10 @@ class OnboardingViewModel setSideEffect(OnboardingContract.OnboardingSideEffect.LaunchAlbum) } + OnboardingContract.OnboardingIntent.OnBackGestureAfterSignup -> { + setSideEffect(OnboardingContract.OnboardingSideEffect.NavigateToLogin) + } + OnboardingContract.OnboardingIntent.OnStartButtonClick -> { setSideEffect(OnboardingContract.OnboardingSideEffect.NavigateToPermission) } @@ -93,7 +104,14 @@ class OnboardingViewModel } is OnboardingContract.OnboardingIntent.OnNotificationPermissionResult -> { - setSideEffect(OnboardingContract.OnboardingSideEffect.NavigateToHome) + viewModelScope.launch { + userRepository + .updateNotificationEnabled(intent.isGranted) + .onSuccess { + setSideEffect(OnboardingContract.OnboardingSideEffect.NavigateToHome) + }.onFailure { + } + } } } } @@ -103,10 +121,18 @@ class OnboardingViewModel authRepository .signupWithKakao( kakaoAccessToken = currentState.kakaoAccessToken, - profileImage = currentState.profileImageUri.toString(), + profileImage = currentState.profileImagePath, nickname = currentState.nickname, - ).onSuccess { - setSideEffect(OnboardingContract.OnboardingSideEffect.NavigateToOnboarding) + ).onSuccess { validationResult -> + if (validationResult is NicknameValidationResult.Error.Duplicated) { + updateState { + copy( + nicknameInputState = NicknameValidationResult.Error.Duplicated.toUiState(), + ) + } + } else if (validationResult is NicknameValidationResult.Success) { + setSideEffect(OnboardingContract.OnboardingSideEffect.NavigateToOnboarding) + } }.onFailure { } } diff --git a/feature/onboarding/src/main/res/drawable/img_onboarding_1.png b/feature/onboarding/src/main/res/drawable/img_onboarding_1.png new file mode 100644 index 00000000..e85f8098 Binary files /dev/null and b/feature/onboarding/src/main/res/drawable/img_onboarding_1.png differ diff --git a/feature/onboarding/src/main/res/drawable/img_onboarding_2.png b/feature/onboarding/src/main/res/drawable/img_onboarding_2.png new file mode 100644 index 00000000..94d92ba4 Binary files /dev/null and b/feature/onboarding/src/main/res/drawable/img_onboarding_2.png differ diff --git a/core/designsystem/src/main/res/drawable/img_onboarding_3.png b/feature/onboarding/src/main/res/drawable/img_onboarding_3.png similarity index 100% rename from core/designsystem/src/main/res/drawable/img_onboarding_3.png rename to feature/onboarding/src/main/res/drawable/img_onboarding_3.png diff --git a/feature/setting/src/main/java/com/example/setting/SettingContract.kt b/feature/setting/src/main/java/com/example/setting/SettingContract.kt index 29b3b57b..c568d25d 100644 --- a/feature/setting/src/main/java/com/example/setting/SettingContract.kt +++ b/feature/setting/src/main/java/com/example/setting/SettingContract.kt @@ -33,5 +33,9 @@ class SettingContract { data object ShowLogoutWarningDialog : SettingSideEffect data object ShowWithdrawWarningDialog : SettingSideEffect + + data class OpenWebView( + val url: String, + ) : SettingSideEffect } } diff --git a/feature/setting/src/main/java/com/example/setting/SettingMenuType.kt b/feature/setting/src/main/java/com/example/setting/SettingMenuType.kt index b8640363..72a2c52f 100644 --- a/feature/setting/src/main/java/com/example/setting/SettingMenuType.kt +++ b/feature/setting/src/main/java/com/example/setting/SettingMenuType.kt @@ -2,15 +2,17 @@ package com.example.setting import androidx.annotation.StringRes import com.dplay.setting.R +import com.example.common.constant.Url enum class SettingMenuType( @StringRes val titleResId: Int, + val url: String? = null, ) { PUSH_NOTIFICATION(R.string.setting_push_notification), - ANNOUNCEMENT(R.string.setting_announcement), - INQUIRY(R.string.setting_inquiry), - TERMS(R.string.setting_terms), - PRIVACY(R.string.setting_privacy), + ANNOUNCEMENT(R.string.setting_announcement, Url.ANNOUNCEMENT), + INQUIRY(R.string.setting_inquiry, Url.INQUIRY), + TERMS(R.string.setting_terms, Url.TERMS_OF_SERVICE), + PRIVACY(R.string.setting_privacy, Url.PRIVACY_POLICY), VERSION(R.string.setting_version), LOGOUT(R.string.setting_logout), WITHDRAW(R.string.setting_withdraw), diff --git a/feature/setting/src/main/java/com/example/setting/SettingScreen.kt b/feature/setting/src/main/java/com/example/setting/SettingScreen.kt index 210de915..1c8b8e32 100644 --- a/feature/setting/src/main/java/com/example/setting/SettingScreen.kt +++ b/feature/setting/src/main/java/com/example/setting/SettingScreen.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -43,6 +44,7 @@ fun SettingRoute( val context = LocalContext.current val modalController = LocalModalController.current + val uriHandler = LocalUriHandler.current LaunchedEffect(Unit) { viewModel.sideEffect.collectLatest { sideEffect -> @@ -82,6 +84,9 @@ fun SettingRoute( rightButtonLabel = context.getString(com.dplay.setting.R.string.withdraw_warning_right_button_label), ) } + is SettingContract.SettingSideEffect.OpenWebView -> { + uriHandler.openUri(sideEffect.url) + } } } } diff --git a/feature/setting/src/main/java/com/example/setting/SettingViewModel.kt b/feature/setting/src/main/java/com/example/setting/SettingViewModel.kt index 6045db2f..c7b54e10 100644 --- a/feature/setting/src/main/java/com/example/setting/SettingViewModel.kt +++ b/feature/setting/src/main/java/com/example/setting/SettingViewModel.kt @@ -1,6 +1,7 @@ package com.example.setting import androidx.lifecycle.viewModelScope +import com.example.common.constant.Url import com.example.domain.repository.AuthRepository import com.example.domain.repository.UserRepository import com.example.ui.base.BaseViewModel @@ -70,16 +71,16 @@ class SettingViewModel toggleNotification(!currentState.isPushNotificationEnabled) } SettingMenuType.ANNOUNCEMENT -> { - // 공지사항 노션 링크로 연결 + setSideEffect(SettingContract.SettingSideEffect.OpenWebView(type.url ?: Url.ERROR)) } SettingMenuType.INQUIRY -> { - // 문의/제안하기 구글폼 링크로 연결 + setSideEffect(SettingContract.SettingSideEffect.OpenWebView(type.url ?: Url.ERROR)) } SettingMenuType.TERMS -> { - // 서비스 이용약관 노션 링크로 연결 + setSideEffect(SettingContract.SettingSideEffect.OpenWebView(type.url ?: Url.ERROR)) } SettingMenuType.PRIVACY -> { - // 개인정보 처리방침 노션 링크로 연결 + setSideEffect(SettingContract.SettingSideEffect.OpenWebView(type.url ?: Url.ERROR)) } SettingMenuType.LOGOUT -> { setSideEffect(SettingContract.SettingSideEffect.ShowLogoutWarningDialog) diff --git a/feature/splash/src/main/java/com/example/splash/SplashViewModel.kt b/feature/splash/src/main/java/com/example/splash/SplashViewModel.kt index ba443cb5..8bd7bcdf 100644 --- a/feature/splash/src/main/java/com/example/splash/SplashViewModel.kt +++ b/feature/splash/src/main/java/com/example/splash/SplashViewModel.kt @@ -51,6 +51,6 @@ class SplashViewModel } companion object { - private const val SPLASH_DELAY_MS = 2000L + private const val SPLASH_DELAY_MS = 1000L } }