diff --git a/app/src/main/java/com/haphap/app/core/designsystem/component/modal/HapHapDialog.kt b/app/src/main/java/com/haphap/app/core/designsystem/component/modal/HapHapDialog.kt index a6c8d17d6..1ecec4f86 100644 --- a/app/src/main/java/com/haphap/app/core/designsystem/component/modal/HapHapDialog.kt +++ b/app/src/main/java/com/haphap/app/core/designsystem/component/modal/HapHapDialog.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties import com.haphap.app.R import com.haphap.app.core.designsystem.component.button.HapHapBasicButton import com.haphap.app.core.designsystem.theme.HapHapTheme @@ -31,6 +32,7 @@ import com.haphap.app.core.designsystem.type.ButtonType * @param content 다이얼로그 본문에 표시할 안내 문구 * @param onDismiss 다이얼로그 바깥 영역 또는 "취소" 버튼 클릭 시 호출되는 콜백 * @param onConfirmClick "확인" 버튼 클릭 시 호출되는 콜백 + * @param showIcon 다이얼로그에 아이콘이 있는 지 설정하는 변수 */ @Composable @@ -39,27 +41,37 @@ fun HapHapDialog( onDismiss: () -> Unit, onConfirmClick: () -> Unit, modifier: Modifier = Modifier, + showIcon: Boolean = false, ) { Dialog ( onDismissRequest = onDismiss, + properties = DialogProperties(usePlatformDefaultWidth = false), ) { Column( modifier = modifier + .padding(horizontal = 31.dp) .fillMaxWidth() .background( color = HapHapTheme.colors.white, shape = RoundedCornerShape(16.dp), ) - .padding(start = 20.dp, top = 16.dp, end = 20.dp, bottom = 21.dp), + .padding( + start = 19.dp, + top = 16.dp, + end = 19.dp, + bottom = if (showIcon) 21.dp else 16.dp, + ), horizontalAlignment = Alignment.CenterHorizontally, ) { - Icon( - imageVector = ImageVector.vectorResource(id = R.drawable.ic_alert_53), - contentDescription = null, - tint = HapHapTheme.colors.gray200, - ) + if (showIcon) { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_alert_53), + contentDescription = null, + tint = HapHapTheme.colors.gray200, + ) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(8.dp)) + } Text( text = content, @@ -72,7 +84,7 @@ fun HapHapDialog( Row( modifier = modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(11.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), ) { HapHapBasicButton( text = "취소", @@ -98,9 +110,10 @@ fun HapHapDialog( private fun HapHapDialogPreview() { HapHapTheme { HapHapDialog( - content = "이전에 등록한 결과가 있습니다.\n결과를 변경할까요?", + content = "로그아웃 하시겠습니까?", onDismiss = {}, onConfirmClick = {}, + showIcon = false, ) } } \ No newline at end of file diff --git a/app/src/main/java/com/haphap/app/core/designsystem/theme/Color.kt b/app/src/main/java/com/haphap/app/core/designsystem/theme/Color.kt index 2faef6499..5f69dac92 100644 --- a/app/src/main/java/com/haphap/app/core/designsystem/theme/Color.kt +++ b/app/src/main/java/com/haphap/app/core/designsystem/theme/Color.kt @@ -22,6 +22,8 @@ val Sub300 = Color(0xFFC3D9FF) val Sub400 = Color(0xFF041B50) val Yellow = Color(0xFFFEE500) +val SubRed = Color(0xFFFF7D7D) + val Gray50 = Color(0xFFFAFBFB) val Gray100 = Color(0xFFF3F4F7) val Gray200 = Color(0xFFD2D6DB) @@ -46,6 +48,7 @@ data class HapHapColors( val sub300: Color, val sub400: Color, val yellow: Color, + val subred: Color, val gray50: Color, val gray100: Color, val gray200: Color, @@ -69,6 +72,7 @@ val defaultHapHapColors = HapHapColors( sub300 = Sub300, sub400 = Sub400, yellow = Yellow, + subred = SubRed, gray50 = Gray50, gray100 = Gray100, gray200 = Gray200, @@ -98,6 +102,7 @@ private fun ColorPreview() { HapHapTheme.colors.sub300, HapHapTheme.colors.sub400, HapHapTheme.colors.yellow, + HapHapTheme.colors.subred, HapHapTheme.colors.gray50, HapHapTheme.colors.gray100, diff --git a/app/src/main/java/com/haphap/app/core/extensions/StringExt.kt b/app/src/main/java/com/haphap/app/core/extensions/StringExt.kt index db8347742..ae55ac9b6 100644 --- a/app/src/main/java/com/haphap/app/core/extensions/StringExt.kt +++ b/app/src/main/java/com/haphap/app/core/extensions/StringExt.kt @@ -1,5 +1,6 @@ package com.haphap.app.core.extensions +import java.text.BreakIterator import java.time.LocalDate import java.time.LocalDateTime import java.time.format.DateTimeFormatter @@ -17,4 +18,24 @@ fun String.toTimeFormat(pattern: String = "HH:mm"): String { return runCatching { LocalDateTime.parse(this).format(DateTimeFormatter.ofPattern(pattern)) }.getOrDefault("") +} + +/** + * 사람이 보는 글자(grapheme cluster) 단위로 길이를 센다. + * + * [CharSequence.length]는 UTF-16 Char 개수를 세기 때문에 이모지 하나가 2개 이상으로 세어지는 반면, + * 이 함수는 "😀", "👍🏻", "👨‍👩‍👧" 같은 이모지도 1글자로 센다. + * + * @receiver 길이를 셀 문자열 + * @return 글자 수 + */ +fun CharSequence.graphemeLength(): Int { + val iterator = BreakIterator.getCharacterInstance().apply { setText(this@graphemeLength.toString()) } + + var count = 0 + while (iterator.next() != BreakIterator.DONE) { + count++ + } + + return count } \ No newline at end of file diff --git a/app/src/main/java/com/haphap/app/data/local/datasource/api/LocalFcmDataSource.kt b/app/src/main/java/com/haphap/app/data/local/datasource/api/LocalFcmDataSource.kt index 3c7260509..f3076e910 100644 --- a/app/src/main/java/com/haphap/app/data/local/datasource/api/LocalFcmDataSource.kt +++ b/app/src/main/java/com/haphap/app/data/local/datasource/api/LocalFcmDataSource.kt @@ -3,4 +3,5 @@ package com.haphap.app.data.local.datasource.api interface LocalFcmDataSource { suspend fun getFcmToken(): String? suspend fun setFcmToken(fcmToken: String) + suspend fun clearFcmToken() } diff --git a/app/src/main/java/com/haphap/app/data/local/datasource/impl/LocalFcmDataSourceImpl.kt b/app/src/main/java/com/haphap/app/data/local/datasource/impl/LocalFcmDataSourceImpl.kt index ae81ab1cb..a1a6982fc 100644 --- a/app/src/main/java/com/haphap/app/data/local/datasource/impl/LocalFcmDataSourceImpl.kt +++ b/app/src/main/java/com/haphap/app/data/local/datasource/impl/LocalFcmDataSourceImpl.kt @@ -24,6 +24,12 @@ class LocalFcmDataSourceImpl @Inject constructor( } } + override suspend fun clearFcmToken() { + dataStore.edit { prefs -> + prefs.remove(FCM_TOKEN) + } + } + companion object { private val FCM_TOKEN = stringPreferencesKey("FCM_TOKEN") } diff --git a/app/src/main/java/com/haphap/app/data/mapper/auth/AuthMapper.kt b/app/src/main/java/com/haphap/app/data/mapper/auth/AuthMapper.kt index a52425442..625e0da38 100644 --- a/app/src/main/java/com/haphap/app/data/mapper/auth/AuthMapper.kt +++ b/app/src/main/java/com/haphap/app/data/mapper/auth/AuthMapper.kt @@ -8,4 +8,5 @@ fun KakaoLoginResponseDto.toModel(): KakaoLoginModel = KakaoLoginModel( refreshToken = refreshToken, name = name, anonymousName = anonymousName, + isNewUser = isNewUser, ) \ No newline at end of file diff --git a/app/src/main/java/com/haphap/app/data/model/auth/KakaoLoginModel.kt b/app/src/main/java/com/haphap/app/data/model/auth/KakaoLoginModel.kt index 8a523433a..bd67b424d 100644 --- a/app/src/main/java/com/haphap/app/data/model/auth/KakaoLoginModel.kt +++ b/app/src/main/java/com/haphap/app/data/model/auth/KakaoLoginModel.kt @@ -5,4 +5,5 @@ data class KakaoLoginModel( val refreshToken: String, val name: String, val anonymousName: String, + val isNewUser: Boolean, ) \ No newline at end of file diff --git a/app/src/main/java/com/haphap/app/data/remote/datasource/api/mypage/MyPageDataSource.kt b/app/src/main/java/com/haphap/app/data/remote/datasource/api/mypage/MyPageDataSource.kt index be3e4db2d..ebb59aff4 100644 --- a/app/src/main/java/com/haphap/app/data/remote/datasource/api/mypage/MyPageDataSource.kt +++ b/app/src/main/java/com/haphap/app/data/remote/datasource/api/mypage/MyPageDataSource.kt @@ -1,8 +1,13 @@ package com.haphap.app.data.remote.datasource.api.mypage import com.haphap.app.data.remote.dto.BaseResponse +import com.haphap.app.data.remote.dto.mypage.LeaveRequestDto +import com.haphap.app.data.remote.dto.mypage.LogoutRequestDto import com.haphap.app.data.remote.dto.mypage.MyPageResponseDto +import retrofit2.Response interface MyPageDataSource{ suspend fun getMyPage(): BaseResponse + suspend fun deleteMember(requestDto: LeaveRequestDto): Response + suspend fun postLogout(requestDto: LogoutRequestDto): Response } \ No newline at end of file diff --git a/app/src/main/java/com/haphap/app/data/remote/datasource/impl/mypage/MyPageDataSourceImpl.kt b/app/src/main/java/com/haphap/app/data/remote/datasource/impl/mypage/MyPageDataSourceImpl.kt index 6f7d39d4e..89c427d74 100644 --- a/app/src/main/java/com/haphap/app/data/remote/datasource/impl/mypage/MyPageDataSourceImpl.kt +++ b/app/src/main/java/com/haphap/app/data/remote/datasource/impl/mypage/MyPageDataSourceImpl.kt @@ -2,9 +2,12 @@ package com.haphap.app.data.remote.datasource.impl.mypage import com.haphap.app.data.remote.datasource.api.mypage.MyPageDataSource import com.haphap.app.data.remote.dto.BaseResponse +import com.haphap.app.data.remote.dto.mypage.LeaveRequestDto +import com.haphap.app.data.remote.dto.mypage.LogoutRequestDto import com.haphap.app.data.remote.dto.mypage.MyPageResponseDto import com.haphap.app.data.remote.service.MyPageService import jakarta.inject.Inject +import retrofit2.Response class MyPageDataSourceImpl @Inject constructor( private val myPageService: MyPageService, @@ -12,4 +15,10 @@ class MyPageDataSourceImpl @Inject constructor( override suspend fun getMyPage(): BaseResponse { return myPageService.getMyPage() } + + override suspend fun deleteMember(requestDto: LeaveRequestDto): Response = + myPageService.deleteMember(requestDto) + + override suspend fun postLogout(requestDto: LogoutRequestDto): Response = + myPageService.logout(requestDto) } \ No newline at end of file diff --git a/app/src/main/java/com/haphap/app/data/remote/dto/auth/KakaoLoginResponseDto.kt b/app/src/main/java/com/haphap/app/data/remote/dto/auth/KakaoLoginResponseDto.kt index 90fb56b3f..d306856f0 100644 --- a/app/src/main/java/com/haphap/app/data/remote/dto/auth/KakaoLoginResponseDto.kt +++ b/app/src/main/java/com/haphap/app/data/remote/dto/auth/KakaoLoginResponseDto.kt @@ -13,4 +13,6 @@ data class KakaoLoginResponseDto( val name: String, @SerialName("anonymousName") val anonymousName: String, + @SerialName("isNewUser") + val isNewUser: Boolean = false, ) \ No newline at end of file diff --git a/app/src/main/java/com/haphap/app/data/remote/dto/mypage/LeaveRequestDto.kt b/app/src/main/java/com/haphap/app/data/remote/dto/mypage/LeaveRequestDto.kt new file mode 100644 index 000000000..ac8edc38e --- /dev/null +++ b/app/src/main/java/com/haphap/app/data/remote/dto/mypage/LeaveRequestDto.kt @@ -0,0 +1,12 @@ +package com.haphap.app.data.remote.dto.mypage + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class LeaveRequestDto( + @SerialName("reason") + val reason: String, + @SerialName("etcReason") + val etcReason: String? = null, +) \ No newline at end of file diff --git a/app/src/main/java/com/haphap/app/data/remote/dto/mypage/LogoutRequestDto.kt b/app/src/main/java/com/haphap/app/data/remote/dto/mypage/LogoutRequestDto.kt new file mode 100644 index 000000000..d94a9bf7c --- /dev/null +++ b/app/src/main/java/com/haphap/app/data/remote/dto/mypage/LogoutRequestDto.kt @@ -0,0 +1,10 @@ +package com.haphap.app.data.remote.dto.mypage + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class LogoutRequestDto( + @SerialName("deviceId") + val deviceId: String, +) \ No newline at end of file diff --git a/app/src/main/java/com/haphap/app/data/remote/service/MyPageService.kt b/app/src/main/java/com/haphap/app/data/remote/service/MyPageService.kt index 0f05104a3..7feff9c12 100644 --- a/app/src/main/java/com/haphap/app/data/remote/service/MyPageService.kt +++ b/app/src/main/java/com/haphap/app/data/remote/service/MyPageService.kt @@ -1,10 +1,22 @@ package com.haphap.app.data.remote.service import com.haphap.app.data.remote.dto.BaseResponse +import com.haphap.app.data.remote.dto.mypage.LeaveRequestDto +import com.haphap.app.data.remote.dto.mypage.LogoutRequestDto import com.haphap.app.data.remote.dto.mypage.MyPageResponseDto +import retrofit2.Response +import retrofit2.http.Body import retrofit2.http.GET +import retrofit2.http.HTTP +import retrofit2.http.POST interface MyPageService { @GET("api/v1/members/me") suspend fun getMyPage(): BaseResponse + + @HTTP(method = "DELETE", path = "api/v1/members/me", hasBody = true) + suspend fun deleteMember(@Body request: LeaveRequestDto): Response + + @POST("api/v1/auth/logout") + suspend fun logout(@Body request: LogoutRequestDto): Response } \ No newline at end of file diff --git a/app/src/main/java/com/haphap/app/data/repository/api/auth/AuthRepository.kt b/app/src/main/java/com/haphap/app/data/repository/api/auth/AuthRepository.kt index 8d3cc9102..08e8f29ee 100644 --- a/app/src/main/java/com/haphap/app/data/repository/api/auth/AuthRepository.kt +++ b/app/src/main/java/com/haphap/app/data/repository/api/auth/AuthRepository.kt @@ -1,7 +1,6 @@ package com.haphap.app.data.repository.api.auth import com.haphap.app.data.model.auth.KakaoLoginModel -import com.haphap.app.data.remote.dto.auth.KakaoLoginRequestDto interface AuthRepository { suspend fun postKakaoLogin(accessToken: String): Result diff --git a/app/src/main/java/com/haphap/app/data/repository/api/mypage/MyPageRepository.kt b/app/src/main/java/com/haphap/app/data/repository/api/mypage/MyPageRepository.kt index 0b2a66f2e..273c751b1 100644 --- a/app/src/main/java/com/haphap/app/data/repository/api/mypage/MyPageRepository.kt +++ b/app/src/main/java/com/haphap/app/data/repository/api/mypage/MyPageRepository.kt @@ -4,4 +4,6 @@ import com.haphap.app.data.model.mypage.MyPageModel interface MyPageRepository { suspend fun getMyPage(): Result + suspend fun deleteMember(reason: String, etcReason: String?): Result + suspend fun postLogout(): Result } \ No newline at end of file diff --git a/app/src/main/java/com/haphap/app/data/repository/impl/mypage/MyPageRepositoryImpl.kt b/app/src/main/java/com/haphap/app/data/repository/impl/mypage/MyPageRepositoryImpl.kt index 60ef58704..73112b45d 100644 --- a/app/src/main/java/com/haphap/app/data/repository/impl/mypage/MyPageRepositoryImpl.kt +++ b/app/src/main/java/com/haphap/app/data/repository/impl/mypage/MyPageRepositoryImpl.kt @@ -1,19 +1,58 @@ package com.haphap.app.data.repository.impl.mypage +import com.haphap.app.core.fcm.FirebaseMessagingManager import com.haphap.app.core.util.suspendRunCatching +import com.haphap.app.data.local.datasource.api.LocalFcmDataSource +import com.haphap.app.data.local.datasource.api.LocalTokenDataSource import com.haphap.app.data.mapper.mypage.toModel import com.haphap.app.data.model.mypage.MyPageModel import com.haphap.app.data.remote.datasource.api.mypage.MyPageDataSource import com.haphap.app.data.remote.dto.checkData +import com.haphap.app.data.remote.dto.mypage.LeaveRequestDto +import com.haphap.app.data.remote.dto.mypage.LogoutRequestDto import com.haphap.app.data.repository.api.mypage.MyPageRepository import jakarta.inject.Inject +import retrofit2.HttpException +import timber.log.Timber class MyPageRepositoryImpl @Inject constructor( private val myPageDataSource: MyPageDataSource, + private val localTokenDataSource: LocalTokenDataSource, + private val localFcmDataSource: LocalFcmDataSource, + private val firebaseMessagingManager: FirebaseMessagingManager, ): MyPageRepository { override suspend fun getMyPage(): Result = suspendRunCatching { val response = myPageDataSource.getMyPage().checkData() response.toModel() } + + override suspend fun deleteMember(reason: String, etcReason: String?): Result = + suspendRunCatching { + val response = myPageDataSource.deleteMember( + LeaveRequestDto(reason = reason, etcReason = etcReason), + ) + + if (!response.isSuccessful && response.code() != 404) throw HttpException(response) + + localTokenDataSource.clearTokens() + } + + override suspend fun postLogout(): Result = + suspendRunCatching { + val accessToken = localTokenDataSource.getAccessToken() + + if (!accessToken.isNullOrBlank()) { + val deviceId = firebaseMessagingManager.getInstallationId() + + if (deviceId != null) { + val response = myPageDataSource.postLogout(LogoutRequestDto(deviceId)) + if (!response.isSuccessful && response.code() != 401) throw HttpException(response) + } else { + Timber.w("Installation id is null. Skip remote logout") + } + } + + localTokenDataSource.clearTokens() + } } \ No newline at end of file diff --git a/app/src/main/java/com/haphap/app/presentation/auth/login/LoginContract.kt b/app/src/main/java/com/haphap/app/presentation/auth/login/LoginContract.kt index 7fe781a12..4ae9673a2 100644 --- a/app/src/main/java/com/haphap/app/presentation/auth/login/LoginContract.kt +++ b/app/src/main/java/com/haphap/app/presentation/auth/login/LoginContract.kt @@ -10,6 +10,7 @@ sealed interface LoginContract { sealed class SideEffect { data class NavigateToSignUpComplete(val userName: String) : SideEffect() + data object NavigateToHome : SideEffect() } } diff --git a/app/src/main/java/com/haphap/app/presentation/auth/login/LoginScreen.kt b/app/src/main/java/com/haphap/app/presentation/auth/login/LoginScreen.kt index 3e8b02b3b..6b0fa2d5e 100644 --- a/app/src/main/java/com/haphap/app/presentation/auth/login/LoginScreen.kt +++ b/app/src/main/java/com/haphap/app/presentation/auth/login/LoginScreen.kt @@ -36,11 +36,13 @@ import com.haphap.app.R import com.haphap.app.core.designsystem.component.circular_progress_indicator.HapHapCircularProgressIndicator import com.haphap.app.core.designsystem.theme.HapHapTheme import com.haphap.app.core.extensions.noRippleClickable +import com.haphap.app.presentation.auth.login.LoginContract.SideEffect.NavigateToHome import com.haphap.app.presentation.auth.login.LoginContract.SideEffect.NavigateToSignUpComplete @Composable fun LoginRoute( navigateToSignUpComplete: (userName: String) -> Unit, + navigateToHome: () -> Unit, modifier: Modifier = Modifier, viewModel: LoginViewModel = hiltViewModel(), ) { @@ -56,6 +58,8 @@ fun LoginRoute( is NavigateToSignUpComplete -> { navigateToSignUpComplete(sideEffect.userName) } + + is NavigateToHome -> navigateToHome() } } } diff --git a/app/src/main/java/com/haphap/app/presentation/auth/login/LoginViewModel.kt b/app/src/main/java/com/haphap/app/presentation/auth/login/LoginViewModel.kt index 7398c96d2..75590d8c6 100644 --- a/app/src/main/java/com/haphap/app/presentation/auth/login/LoginViewModel.kt +++ b/app/src/main/java/com/haphap/app/presentation/auth/login/LoginViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.haphap.app.data.repository.api.alarm.AlarmRepository import com.haphap.app.data.repository.api.auth.AuthRepository +import com.haphap.app.presentation.auth.login.LoginContract.SideEffect.NavigateToHome import com.haphap.app.presentation.auth.login.LoginContract.SideEffect.NavigateToSignUpComplete import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.channels.Channel @@ -37,7 +38,11 @@ class LoginViewModel @Inject constructor( .onSuccess { model -> alarmRepository.registerDeviceId() _uiState.update { it.copy(loginUiState = LoginUiState.Success) } - _sideEffect.send(NavigateToSignUpComplete(model.name)) + if (model.isNewUser) { + _sideEffect.send(NavigateToSignUpComplete(model.name)) + } else { + _sideEffect.send(NavigateToHome) + } } .onFailure { throwable -> _uiState.update { diff --git a/app/src/main/java/com/haphap/app/presentation/auth/navigation/AuthNavigation.kt b/app/src/main/java/com/haphap/app/presentation/auth/navigation/AuthNavigation.kt index dd8d785fa..b4605f735 100644 --- a/app/src/main/java/com/haphap/app/presentation/auth/navigation/AuthNavigation.kt +++ b/app/src/main/java/com/haphap/app/presentation/auth/navigation/AuthNavigation.kt @@ -36,6 +36,11 @@ fun NavGraphBuilder.authGraph( navOptions = navController.clearBackStackNavOptions(), ) }, + navigateToHome = { + navController.navigateToHome( + navOptions = navController.clearBackStackNavOptions() + ) + }, modifier = Modifier.padding(innerPadding), ) } diff --git a/app/src/main/java/com/haphap/app/presentation/leave/LeaveCompleteScreen.kt b/app/src/main/java/com/haphap/app/presentation/leave/LeaveCompleteScreen.kt new file mode 100644 index 000000000..7ae066742 --- /dev/null +++ b/app/src/main/java/com/haphap/app/presentation/leave/LeaveCompleteScreen.kt @@ -0,0 +1,92 @@ +package com.haphap.app.presentation.leave + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +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 +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.haphap.app.R +import com.haphap.app.core.designsystem.component.button.HapHapBasicButton +import com.haphap.app.core.designsystem.theme.HapHapTheme +import com.haphap.app.core.designsystem.type.ButtonType + +@Composable +fun LeaveCompleteRoute( + navigateToLogin: () -> Unit, + modifier: Modifier = Modifier, +) { + LeaveCompleteScreen( + navigateToLogin = navigateToLogin, + modifier = modifier, + ) +} + +@Composable +fun LeaveCompleteScreen( + navigateToLogin: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxSize() + .background(HapHapTheme.colors.white) + .padding(start = 20.dp, end = 20.dp, top = 36.dp), + ) { + Text( + text = "회원 탈퇴가 완료됐어요.", + style = HapHapTheme.typography.subtitle.b22, + color = HapHapTheme.colors.gray800, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "앞으로의 여정도 응원할게요.\n언제든 다시 만나요!", + style = HapHapTheme.typography.body.m14, + color = HapHapTheme.colors.gray500, + ) + + Spacer(modifier = Modifier.weight(132f / 342f)) + + Image( + painter = painterResource(R.drawable.img_register_check), + contentDescription = null, + modifier = Modifier + .size(210.dp) + .align(Alignment.CenterHorizontally), + ) + + Spacer(modifier = Modifier.weight(210f / 342f)) + + HapHapBasicButton( + text = "완료", + textStyle = HapHapTheme.typography.body.b18, + colorType = ButtonType.Primary(enabled = true), + onClick = navigateToLogin, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 10.dp) + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun LeaveCompleteScreenPreview() { + HapHapTheme { + LeaveCompleteScreen( + navigateToLogin = {}, + ) + } +} diff --git a/app/src/main/java/com/haphap/app/presentation/leave/LeaveContract.kt b/app/src/main/java/com/haphap/app/presentation/leave/LeaveContract.kt new file mode 100644 index 000000000..97d5cba71 --- /dev/null +++ b/app/src/main/java/com/haphap/app/presentation/leave/LeaveContract.kt @@ -0,0 +1,25 @@ +package com.haphap.app.presentation.leave + +import androidx.compose.runtime.Immutable +import com.haphap.app.presentation.leave.type.LeaveReasonType + +sealed interface LeaveContract { + @Immutable + data class State( + val selectedReason: LeaveReasonType? = null, + val isEtcReasonBlank: Boolean = true, + val isLoading: Boolean = false, + ) { + val isButtonEnabled: Boolean + get() = !isLoading && when (selectedReason) { + null -> false + LeaveReasonType.ETC -> !isEtcReasonBlank + else -> true + } + } + + sealed interface SideEffect { + data object NavigateToSetting : SideEffect + data object NavigateToLeaveComplete : SideEffect + } +} \ No newline at end of file diff --git a/app/src/main/java/com/haphap/app/presentation/leave/LeaveScreen.kt b/app/src/main/java/com/haphap/app/presentation/leave/LeaveScreen.kt new file mode 100644 index 000000000..5c9e7c410 --- /dev/null +++ b/app/src/main/java/com/haphap/app/presentation/leave/LeaveScreen.kt @@ -0,0 +1,167 @@ +package com.haphap.app.presentation.leave + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.haphap.app.core.designsystem.component.button.HapHapBasicButton +import com.haphap.app.core.designsystem.theme.HapHapTheme +import com.haphap.app.core.designsystem.type.ButtonType +import com.haphap.app.presentation.leave.LeaveContract.SideEffect.NavigateToLeaveComplete +import com.haphap.app.presentation.leave.LeaveContract.SideEffect.NavigateToSetting +import com.haphap.app.presentation.leave.component.LeaveCheckItem +import com.haphap.app.presentation.leave.component.LeaveTopBar +import com.haphap.app.presentation.leave.type.LeaveReasonType + +@Composable +fun LeaveRoute( + navigateBack: () -> Unit, + navigateToLeaveComplete: () -> Unit, + modifier: Modifier = Modifier, + viewModel: LeaveViewModel = hiltViewModel(), +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val lifecycleOwner = LocalLifecycleOwner.current + + LaunchedEffect(lifecycleOwner) { + lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.sideEffect.collect { sideEffect -> + when (sideEffect) { + is NavigateToSetting -> navigateBack() + is NavigateToLeaveComplete -> navigateToLeaveComplete() + } + } + } + } + + LeaveScreen( + uiState = uiState, + etcReasonState = viewModel.etcReasonState, + onBackClick = viewModel::onBackClick, + onReasonClick = viewModel::onReasonClick, + onLeaveClick = viewModel::onLeaveClick, + modifier = modifier, + ) +} + +@Composable +private fun LeaveScreen( + uiState: LeaveContract.State, + etcReasonState: TextFieldState, + onBackClick: () -> Unit, + onReasonClick: (LeaveReasonType) -> Unit, + onLeaveClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxSize() + .imePadding() + .padding(horizontal = 20.dp), + ) { + LeaveTopBar( + onBackClick = onBackClick, + ) + + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .verticalScroll(rememberScrollState()), + ) { + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "탈퇴 유의사항", + style = HapHapTheme.typography.caption.m12, + color = HapHapTheme.colors.gray400, + ) + + Spacer(modifier = Modifier.height(24.dp)) + + // TODO: 탈퇴 유의 사항 기입 + Row( + modifier = Modifier + .fillMaxWidth() + .height(400.dp) + .background( + color = HapHapTheme.colors.gray200, + shape = RoundedCornerShape(19.dp), + ) + ) {} + + Spacer(modifier = Modifier.height(12.dp)) + + HorizontalDivider( + color = HapHapTheme.colors.gray200, + thickness = 1.dp, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "탈퇴 사유를 선택해주세요(필수)", + style = HapHapTheme.typography.body.sb14, + color = HapHapTheme.colors.gray700, + ) + + Spacer(modifier = Modifier.height(12.dp)) + + LeaveReasonType.entries.forEach { reason -> + LeaveCheckItem( + context = reason.text, + onCheckedChange = { onReasonClick(reason) }, + checked = reason == uiState.selectedReason, + isEtc = reason == LeaveReasonType.ETC, + etcReasonState = etcReasonState, + ) + } + + Spacer(modifier = Modifier.height(2.dp)) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + HapHapBasicButton( + text = "취소", + onClick = onBackClick, + modifier = Modifier.weight(1f), + textStyle = HapHapTheme.typography.body.b18, + colorType = ButtonType.Cancel, + ) + HapHapBasicButton( + text = "탈퇴하기", + onClick = onLeaveClick, + modifier = Modifier.weight(1f), + textStyle = HapHapTheme.typography.body.b18, + colorType = ButtonType.Primary(enabled = uiState.isButtonEnabled), + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/haphap/app/presentation/leave/LeaveViewModel.kt b/app/src/main/java/com/haphap/app/presentation/leave/LeaveViewModel.kt new file mode 100644 index 000000000..e028db5a0 --- /dev/null +++ b/app/src/main/java/com/haphap/app/presentation/leave/LeaveViewModel.kt @@ -0,0 +1,76 @@ +package com.haphap.app.presentation.leave + +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.runtime.snapshotFlow +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.haphap.app.data.repository.api.mypage.MyPageRepository +import com.haphap.app.presentation.leave.LeaveContract.SideEffect.NavigateToLeaveComplete +import com.haphap.app.presentation.leave.LeaveContract.SideEffect.NavigateToSetting +import com.haphap.app.presentation.leave.type.LeaveReasonType +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class LeaveViewModel @Inject constructor( + private val myPageRepository: MyPageRepository, +) : ViewModel() { + private val _uiState = MutableStateFlow(LeaveContract.State()) + val uiState: StateFlow = _uiState.asStateFlow() + + private val _sideEffect = Channel() + val sideEffect = _sideEffect.receiveAsFlow() + + val etcReasonState = TextFieldState() + + init { + observeEtcReason() + } + + private fun observeEtcReason() = viewModelScope.launch { + snapshotFlow { etcReasonState.text.isBlank() } + .collect { isBlank -> + _uiState.update { it.copy(isEtcReasonBlank = isBlank) } + } + } + + fun onBackClick() { + viewModelScope.launch { + _sideEffect.send(NavigateToSetting) + } + } + + fun onReasonClick(reason: LeaveReasonType) { + _uiState.update { state -> + state.copy(selectedReason = if (state.selectedReason == reason) null else reason) + } + } + + fun onLeaveClick() { + val state = _uiState.value + if (!state.isButtonEnabled) return + + val reason = state.selectedReason ?: return + + _uiState.update { it.copy(isLoading = true) } + + viewModelScope.launch { + myPageRepository.deleteMember( + reason = reason.serverValue, + etcReason = if (reason == LeaveReasonType.ETC) { + etcReasonState.text.toString().trim() + } else { + null + }, + ).onSuccess { _sideEffect.send(NavigateToLeaveComplete) } + .onFailure { _uiState.update { it.copy(isLoading = false) } } + } + } +} diff --git a/app/src/main/java/com/haphap/app/presentation/leave/component/LeaveCheckItem.kt b/app/src/main/java/com/haphap/app/presentation/leave/component/LeaveCheckItem.kt new file mode 100644 index 000000000..4fe363f00 --- /dev/null +++ b/app/src/main/java/com/haphap/app/presentation/leave/component/LeaveCheckItem.kt @@ -0,0 +1,136 @@ +package com.haphap.app.presentation.leave.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.input.InputTransformation +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.haphap.app.R +import com.haphap.app.core.designsystem.component.textfield.HapHapBasicTextField +import com.haphap.app.core.designsystem.theme.HapHapTheme +import com.haphap.app.core.extensions.graphemeLength +import com.haphap.app.core.extensions.noRippleClickable + +private const val ETC_REASON_MAX_LENGTH = 150 + +@Composable +fun LeaveCheckItem( + context: String, + onCheckedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, + checked: Boolean = false, + isEtc: Boolean = false, + etcReasonState: TextFieldState = rememberTextFieldState(), +) { + Column( + modifier = modifier + .fillMaxWidth() + .padding(vertical = 10.dp), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .noRippleClickable( + onClick = { onCheckedChange(!checked) } + ), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = ImageVector.vectorResource( + id = if (checked) { + R.drawable.ic_check_list_selected_24 + } else { + R.drawable.ic_check_list_default_24 + } + ), + contentDescription = null, + tint = Color.Unspecified, + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = context, + color = HapHapTheme.colors.gray600, + style = HapHapTheme.typography.body.sb14, + ) + } + + if (isEtc && checked) { + Spacer(modifier = Modifier.height(12.dp)) + + Column( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 103.dp) + .background( + color = HapHapTheme.colors.gray100, + shape = RoundedCornerShape(12.dp), + ) + .padding(horizontal = 10.dp, vertical = 12.dp), + verticalArrangement = Arrangement.SpaceBetween, + ) { + HapHapBasicTextField( + state = etcReasonState, + textColor = HapHapTheme.colors.gray500, + textStyle = HapHapTheme.typography.caption.r11, + placeholder = "최대 150자까지 입력이 가능해요.", + placeholderColor = HapHapTheme.colors.gray300, + placeholderStyle = HapHapTheme.typography.caption.r11, + inputTransformation = InputTransformation { + if (asCharSequence().trimStart().graphemeLength() > ETC_REASON_MAX_LENGTH) revertAllChanges() + }, + modifier = Modifier + .fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = "${etcReasonState.text.trimStart().graphemeLength()}/$ETC_REASON_MAX_LENGTH", + color = HapHapTheme.colors.gray400, + style = HapHapTheme.typography.caption.r11, + modifier = Modifier.align(Alignment.End), + ) + } + } + } +} + +@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF) +@Composable +private fun LeaveCheckItemPreview() { + var checked by remember { mutableStateOf(true) } + + HapHapTheme { + LeaveCheckItem( + context = "기타 (직접 입력)", + onCheckedChange = { checked = it }, + checked = checked, + isEtc = true, + ) + } +} diff --git a/app/src/main/java/com/haphap/app/presentation/leave/component/LeaveTopBar.kt b/app/src/main/java/com/haphap/app/presentation/leave/component/LeaveTopBar.kt new file mode 100644 index 000000000..47c38575c --- /dev/null +++ b/app/src/main/java/com/haphap/app/presentation/leave/component/LeaveTopBar.kt @@ -0,0 +1,61 @@ +package com.haphap.app.presentation.leave.component + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +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.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.haphap.app.R +import com.haphap.app.core.designsystem.theme.HapHapTheme +import com.haphap.app.core.extensions.noRippleClickable + +@Composable +fun LeaveTopBar( + onBackClick: () -> Unit, + modifier: Modifier = Modifier, + isText: Boolean = true, +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_back_30), + contentDescription = null, + tint = Color.Unspecified, + modifier = Modifier.noRippleClickable(onClick = onBackClick), + ) + + Spacer(modifier = Modifier.width(10.dp)) + + if (isText) { + Text( + text = "회원 탈퇴", + style = HapHapTheme.typography.subtitle.b20, + color = HapHapTheme.colors.gray800, + ) + } + } +} + +@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF) +@Composable +private fun LeaveTopBarPreview() { + HapHapTheme { + LeaveTopBar( + onBackClick = {}, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/haphap/app/presentation/leave/navigation/LeaveNavigation.kt b/app/src/main/java/com/haphap/app/presentation/leave/navigation/LeaveNavigation.kt new file mode 100644 index 000000000..540768dfd --- /dev/null +++ b/app/src/main/java/com/haphap/app/presentation/leave/navigation/LeaveNavigation.kt @@ -0,0 +1,57 @@ +package com.haphap.app.presentation.leave.navigation + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.ui.Modifier +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.haphap.app.core.extensions.clearBackStackNavOptions +import com.haphap.app.core.navigation.Route +import com.haphap.app.presentation.auth.navigation.navigateToLogin +import com.haphap.app.presentation.leave.LeaveCompleteRoute +import com.haphap.app.presentation.leave.LeaveRoute +import kotlinx.serialization.Serializable + +fun NavController.navigateToLeave( + navOptions: NavOptions? = null, +) = navigate(Leave, navOptions) + +fun NavController.navigateToLeaveComplete( + navOptions: NavOptions? = null, +) = navigate(LeaveComplete, navOptions) + +fun NavGraphBuilder.leaveGraph( + innerPadding: PaddingValues, + navController: NavController, +) { + composable { + LeaveRoute( + navigateBack = navController::popBackStack, + navigateToLeaveComplete = { + navController.navigateToLeaveComplete( + navOptions = navController.clearBackStackNavOptions(), + ) + }, + modifier = Modifier.padding(innerPadding), + ) + } + + composable { + LeaveCompleteRoute( + navigateToLogin = { + navController.navigateToLogin( + navOptions = navController.clearBackStackNavOptions(), + ) + }, + modifier = Modifier.padding(innerPadding), + ) + } +} + +@Serializable +data object Leave : Route + +@Serializable +data object LeaveComplete : Route diff --git a/app/src/main/java/com/haphap/app/presentation/leave/type/LeaveReasonType.kt b/app/src/main/java/com/haphap/app/presentation/leave/type/LeaveReasonType.kt new file mode 100644 index 000000000..99d1c1b1e --- /dev/null +++ b/app/src/main/java/com/haphap/app/presentation/leave/type/LeaveReasonType.kt @@ -0,0 +1,9 @@ +package com.haphap.app.presentation.leave.type + +enum class LeaveReasonType(val text: String, val serverValue: String) { + JOB_SEARCH_FINISHED("취업 준비 활동이 끝났어요.", "JOB_PREPARATION_FINISHED"), + LACK_OF_INFORMATION("원하는 정보가 부족해요.", "NOT_ENOUGH_INFO"), + RARELY_USED("서비스를 잘 사용하지 않아요.", "LOW_USAGE"), + PRIVACY_CONCERN("개인정보(보안) 유출이 걱정돼요.", "PRIVACY_CONCERN"), + ETC("기타 (직접 입력)", "ETC"), +} diff --git a/app/src/main/java/com/haphap/app/presentation/main/MainNavHost.kt b/app/src/main/java/com/haphap/app/presentation/main/MainNavHost.kt index e575b4d82..55cb69736 100644 --- a/app/src/main/java/com/haphap/app/presentation/main/MainNavHost.kt +++ b/app/src/main/java/com/haphap/app/presentation/main/MainNavHost.kt @@ -12,9 +12,11 @@ import com.haphap.app.presentation.calendar.navigation.calendarGraph import com.haphap.app.presentation.home.navigation.homeGraph import com.haphap.app.presentation.joblist.navigation.jobListGraph import com.haphap.app.presentation.jobdetail.navigation.jobDetailGraph +import com.haphap.app.presentation.leave.navigation.leaveGraph import com.haphap.app.presentation.mypage.navigation.myPageGraph import com.haphap.app.presentation.register.navigation.registerGraph import com.haphap.app.presentation.search.navigation.searchGraph +import com.haphap.app.presentation.setting.navigation.settingGraph import com.haphap.app.presentation.splash.navigation.splashGraph private const val NavigationAnimationDurationMillis = 700 @@ -74,6 +76,17 @@ fun MainNavHost( myPageGraph( innerPadding = innerPadding, + navController = navController, + ) + + settingGraph( + innerPadding = innerPadding, + navController = navController, + ) + + leaveGraph( + innerPadding = innerPadding, + navController = navController, ) searchGraph( diff --git a/app/src/main/java/com/haphap/app/presentation/mypage/MyPageContract.kt b/app/src/main/java/com/haphap/app/presentation/mypage/MyPageContract.kt index 6109b4cc2..3bbadcba2 100644 --- a/app/src/main/java/com/haphap/app/presentation/mypage/MyPageContract.kt +++ b/app/src/main/java/com/haphap/app/presentation/mypage/MyPageContract.kt @@ -1,7 +1,6 @@ package com.haphap.app.presentation.mypage import androidx.compose.runtime.Immutable -import com.haphap.app.data.model.mypage.MyPageModel sealed interface MyPageContract { @Immutable @@ -12,6 +11,11 @@ sealed interface MyPageContract { val emailText: String = "", val myPageUiState: MyPageUiState = MyPageUiState.Idle, ) + + sealed class SideEffect { + data object NavigateToHome : SideEffect() + data object NavigateToSetting : SideEffect() + } } sealed interface MyPageUiState { diff --git a/app/src/main/java/com/haphap/app/presentation/mypage/MyPageScreen.kt b/app/src/main/java/com/haphap/app/presentation/mypage/MyPageScreen.kt index 26026b6c6..e7502e946 100644 --- a/app/src/main/java/com/haphap/app/presentation/mypage/MyPageScreen.kt +++ b/app/src/main/java/com/haphap/app/presentation/mypage/MyPageScreen.kt @@ -4,27 +4,49 @@ 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.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.repeatOnLifecycle import com.haphap.app.core.designsystem.theme.HapHapTheme +import com.haphap.app.presentation.mypage.MyPageContract.SideEffect.NavigateToHome +import com.haphap.app.presentation.mypage.MyPageContract.SideEffect.NavigateToSetting import com.haphap.app.presentation.mypage.component.MyPageProfileCard +import com.haphap.app.presentation.mypage.component.MyPageTopBar import com.haphap.app.presentation.mypage.component.MyPageTopSection @Composable fun MyPageRoute( + navigateToHome: () -> Unit, + navigateToSetting: () -> Unit, modifier: Modifier = Modifier, viewModel: MyPageViewModel = hiltViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val lifecycleOwner = LocalLifecycleOwner.current + + LaunchedEffect(lifecycleOwner) { + lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.sideEffect.collect { sideEffect -> + when (sideEffect) { + is NavigateToHome -> navigateToHome() + is NavigateToSetting -> navigateToSetting() + } + } + } + } MyPageScreen( uiState = uiState, + onBackClick = viewModel::onBackClick, + onSettingClick = viewModel::onSettingClick, modifier = modifier, ) } @@ -32,13 +54,20 @@ fun MyPageRoute( @Composable private fun MyPageScreen( uiState: MyPageContract.State, + onBackClick: () -> Unit, + onSettingClick: () -> Unit, modifier: Modifier = Modifier, ) { Column( - modifier = modifier - .fillMaxSize() - .padding(top = 43.dp), + modifier = modifier.fillMaxSize(), ) { + MyPageTopBar( + onBackClick = onBackClick, + onSettingClick = onSettingClick, + ) + + Spacer(modifier = Modifier.height(20.dp)) + MyPageTopSection( nameText = uiState.nameText, ) @@ -62,6 +91,8 @@ private fun MyPageScreenPreview() { nickNameText = "익명의죠르디", emailText = "yeonsoo1234@naver.com", ), + onBackClick = {}, + onSettingClick = {}, ) } } diff --git a/app/src/main/java/com/haphap/app/presentation/mypage/MyPageViewModel.kt b/app/src/main/java/com/haphap/app/presentation/mypage/MyPageViewModel.kt index 5376c235b..baf02b4b8 100644 --- a/app/src/main/java/com/haphap/app/presentation/mypage/MyPageViewModel.kt +++ b/app/src/main/java/com/haphap/app/presentation/mypage/MyPageViewModel.kt @@ -8,7 +8,11 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch +import com.haphap.app.presentation.mypage.MyPageContract.SideEffect.NavigateToHome +import com.haphap.app.presentation.mypage.MyPageContract.SideEffect.NavigateToSetting import timber.log.Timber import javax.inject.Inject @@ -19,6 +23,9 @@ class MyPageViewModel @Inject constructor( private val _uiState = MutableStateFlow(MyPageContract.State()) val uiState: StateFlow = _uiState.asStateFlow() + private val _sideEffect = Channel() + val sideEffect = _sideEffect.receiveAsFlow() + init { getMyPage() } @@ -51,4 +58,16 @@ class MyPageViewModel @Inject constructor( _uiState.update { it.copy(myPageUiState = MyPageUiState.Loading) } } + + fun onBackClick() { + viewModelScope.launch { + _sideEffect.send(NavigateToHome) + } + } + + fun onSettingClick() { + viewModelScope.launch { + _sideEffect.send(NavigateToSetting) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/haphap/app/presentation/mypage/component/MyPageTopBar.kt b/app/src/main/java/com/haphap/app/presentation/mypage/component/MyPageTopBar.kt new file mode 100644 index 000000000..22a3b0c6b --- /dev/null +++ b/app/src/main/java/com/haphap/app/presentation/mypage/component/MyPageTopBar.kt @@ -0,0 +1,70 @@ +package com.haphap.app.presentation.mypage.component + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +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.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.haphap.app.R +import com.haphap.app.core.designsystem.theme.HapHapTheme +import com.haphap.app.core.extensions.noRippleClickable + +@Composable +fun MyPageTopBar( + onBackClick: () -> Unit, + onSettingClick: () -> Unit, + modifier: Modifier = Modifier, + isText: Boolean = true, +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_back_30), + contentDescription = null, + tint = Color.Unspecified, + modifier = Modifier.noRippleClickable(onClick = onBackClick), + ) + + Spacer(modifier = Modifier.width(10.dp)) + + if (isText) Text( + text = "마이페이지", + style = HapHapTheme.typography.subtitle.b20, + color = HapHapTheme.colors.gray800, + ) + + Spacer(modifier = Modifier.weight(1f)) + + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_setting_32), + contentDescription = null, + tint = Color.Unspecified, + modifier = Modifier.noRippleClickable(onClick = onSettingClick), + ) + } +} + +@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF) +@Composable +private fun MyPageTopBarPreview() { + HapHapTheme { + MyPageTopBar( + onBackClick = {}, + onSettingClick = {}, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/haphap/app/presentation/mypage/navigation/MyPageNavigation.kt b/app/src/main/java/com/haphap/app/presentation/mypage/navigation/MyPageNavigation.kt index 565715bee..899bce8a5 100644 --- a/app/src/main/java/com/haphap/app/presentation/mypage/navigation/MyPageNavigation.kt +++ b/app/src/main/java/com/haphap/app/presentation/mypage/navigation/MyPageNavigation.kt @@ -7,8 +7,11 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable +import com.haphap.app.core.extensions.clearBackStackNavOptions import com.haphap.app.core.navigation.MainTabRoute +import com.haphap.app.presentation.home.navigation.navigateToHome import com.haphap.app.presentation.mypage.MyPageRoute +import com.haphap.app.presentation.setting.navigation.navigateToSetting import kotlinx.serialization.Serializable fun NavController.navigateToMyPage( @@ -17,9 +20,16 @@ fun NavController.navigateToMyPage( fun NavGraphBuilder.myPageGraph( innerPadding: PaddingValues, + navController: NavController, ) { composable { MyPageRoute( + navigateToHome = { + navController.navigateToHome( + navOptions = navController.clearBackStackNavOptions() + ) + }, + navigateToSetting = navController::navigateToSetting, modifier = Modifier.padding(innerPadding), ) } diff --git a/app/src/main/java/com/haphap/app/presentation/register/component/RegisterSecondSection.kt b/app/src/main/java/com/haphap/app/presentation/register/component/RegisterSecondSection.kt index 0b48fb42b..07de8d9fe 100644 --- a/app/src/main/java/com/haphap/app/presentation/register/component/RegisterSecondSection.kt +++ b/app/src/main/java/com/haphap/app/presentation/register/component/RegisterSecondSection.kt @@ -60,6 +60,7 @@ fun RegisterSecondSection( content = "이전에 등록한 결과가 있습니다.\n선택한 결과로 변경할까요?", onDismiss = onChangeModalCancelClick, onConfirmClick = onChangeModalConfirmClick, + showIcon = true, ) } } diff --git a/app/src/main/java/com/haphap/app/presentation/setting/SettingContract.kt b/app/src/main/java/com/haphap/app/presentation/setting/SettingContract.kt new file mode 100644 index 000000000..68fe84d95 --- /dev/null +++ b/app/src/main/java/com/haphap/app/presentation/setting/SettingContract.kt @@ -0,0 +1,26 @@ +package com.haphap.app.presentation.setting + +import androidx.compose.runtime.Immutable + +sealed interface SettingContract { + @Immutable + data class State( + val settingUiState: SettingUiState = SettingUiState.Idle, + val isLogoutDialogVisible: Boolean = false, + ) + + sealed interface SideEffect { + data object NavigateToMyPage : SideEffect + data object NavigateToLeave : SideEffect + data object NavigateToLogin : SideEffect + } +} + +sealed interface SettingUiState { + data object Idle : SettingUiState + data object Loading : SettingUiState + data object Success : SettingUiState + data class Failure( + val msg: String, + ) : SettingUiState +} \ No newline at end of file diff --git a/app/src/main/java/com/haphap/app/presentation/setting/SettingScreen.kt b/app/src/main/java/com/haphap/app/presentation/setting/SettingScreen.kt new file mode 100644 index 000000000..232d95fdd --- /dev/null +++ b/app/src/main/java/com/haphap/app/presentation/setting/SettingScreen.kt @@ -0,0 +1,120 @@ +package com.haphap.app.presentation.setting + +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.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.haphap.app.core.designsystem.component.modal.HapHapDialog +import com.haphap.app.core.designsystem.theme.HapHapTheme +import com.haphap.app.presentation.setting.SettingContract.SideEffect.NavigateToLeave +import com.haphap.app.presentation.setting.SettingContract.SideEffect.NavigateToMyPage +import com.haphap.app.presentation.setting.SettingContract.SideEffect.NavigateToLogin +import com.haphap.app.presentation.setting.component.SettingMenuItem +import com.haphap.app.presentation.setting.component.SettingSectionDivider +import com.haphap.app.presentation.setting.component.SettingSectionTitle +import com.haphap.app.presentation.setting.component.SettingTopBar + +@Composable +fun SettingRoute( + navigateBack: () -> Unit, + navigateToLeave: () -> Unit, + navigateToLogin: () -> Unit, + modifier: Modifier = Modifier, + viewModel: SettingViewModel = hiltViewModel(), +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val lifecycleOwner = LocalLifecycleOwner.current + + LaunchedEffect(lifecycleOwner) { + lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.sideEffect.collect { sideEffect -> + when (sideEffect) { + is NavigateToMyPage -> navigateBack() + is NavigateToLeave -> navigateToLeave() + is NavigateToLogin -> navigateToLogin() + } + } + } + } + + SettingScreen( + uiState = uiState, + onBackClick = viewModel::onBackClick, + onLogoutClick = viewModel::onLogoutClick, + onLogoutDialogDismiss = viewModel::onLogoutDialogDismiss, + onLogoutConfirm = viewModel::onLogoutConfirm, + onLeaveClick = viewModel::onLeaveClick, + modifier = modifier, + ) +} + +@Composable +private fun SettingScreen( + uiState: SettingContract.State, + onBackClick: () -> Unit, + onLogoutClick: () -> Unit, + onLogoutDialogDismiss: () -> Unit, + onLogoutConfirm: () -> Unit, + onLeaveClick: () -> Unit, + modifier: Modifier = Modifier, +) { + if (uiState.isLogoutDialogVisible) { + HapHapDialog( + content = "로그아웃 하시겠습니까?", + onDismiss = onLogoutDialogDismiss, + onConfirmClick = onLogoutConfirm, + ) + } + + Column( + modifier = modifier + .fillMaxSize() + .padding(horizontal = 20.dp), + ) { + SettingTopBar( + onBackClick = onBackClick, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + SettingSectionTitle(text = "계정") + + Spacer(modifier = Modifier.height(12.dp)) + + SettingMenuItem(text = "계정 정보") + + SettingSectionDivider() + + SettingSectionTitle(text = "서비스 안내") + + Spacer(modifier = Modifier.height(12.dp)) + + SettingMenuItem(text = "이용 약관") + SettingMenuItem(text = "개인정보 처리 방침") + SettingMenuItem(text = "커뮤니티 운영 정책") + SettingMenuItem(text = "문의하기") + + SettingSectionDivider() + + SettingMenuItem( + text = "로그아웃", + textColor = HapHapTheme.colors.subred, + onClick = onLogoutClick, + ) + SettingMenuItem( + text = "회원 탈퇴", + onClick = onLeaveClick, + ) + } +} diff --git a/app/src/main/java/com/haphap/app/presentation/setting/SettingViewModel.kt b/app/src/main/java/com/haphap/app/presentation/setting/SettingViewModel.kt new file mode 100644 index 000000000..a48772e00 --- /dev/null +++ b/app/src/main/java/com/haphap/app/presentation/setting/SettingViewModel.kt @@ -0,0 +1,69 @@ +package com.haphap.app.presentation.setting + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.haphap.app.data.repository.api.mypage.MyPageRepository +import com.haphap.app.presentation.setting.SettingContract.SideEffect.NavigateToLeave +import com.haphap.app.presentation.setting.SettingContract.SideEffect.NavigateToMyPage +import com.haphap.app.presentation.setting.SettingContract.SideEffect.NavigateToLogin +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SettingViewModel @Inject constructor( + private val myPageRepository: MyPageRepository, +) : ViewModel() { + private val _uiState = MutableStateFlow(SettingContract.State()) + val uiState: StateFlow = _uiState.asStateFlow() + + private val _sideEffect = Channel() + val sideEffect = _sideEffect.receiveAsFlow() + + fun onBackClick() { + viewModelScope.launch { + _sideEffect.send(NavigateToMyPage) + } + } + + fun onLogoutClick() { + if (_uiState.value.settingUiState is SettingUiState.Loading) return + _uiState.update { it.copy(isLogoutDialogVisible = true) } + } + + fun onLogoutDialogDismiss() { + _uiState.update { it.copy(isLogoutDialogVisible = false) } + } + + fun onLogoutConfirm() { + if (_uiState.value.settingUiState is SettingUiState.Loading) return + _uiState.update { + it.copy( + isLogoutDialogVisible = false, + settingUiState = SettingUiState.Loading, + ) + } + + viewModelScope.launch { + myPageRepository.postLogout() + .onSuccess { _sideEffect.send(NavigateToLogin) } + .onFailure { + _uiState.update { + it.copy(settingUiState = SettingUiState.Idle) + } + } + } + } + + fun onLeaveClick() { + viewModelScope.launch { + _sideEffect.send(NavigateToLeave) + } + } +} diff --git a/app/src/main/java/com/haphap/app/presentation/setting/component/SettingMenuItem.kt b/app/src/main/java/com/haphap/app/presentation/setting/component/SettingMenuItem.kt new file mode 100644 index 000000000..1de180b1f --- /dev/null +++ b/app/src/main/java/com/haphap/app/presentation/setting/component/SettingMenuItem.kt @@ -0,0 +1,38 @@ +package com.haphap.app.presentation.setting.component + +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.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.haphap.app.core.designsystem.theme.HapHapTheme +import com.haphap.app.core.extensions.noRippleClickable + +@Composable +fun SettingMenuItem( + text: String, + modifier: Modifier = Modifier, + textColor: Color = HapHapTheme.colors.gray700, + onClick: () -> Unit = {}, +) { + Text( + text = text, + modifier = modifier + .fillMaxWidth() + .noRippleClickable(onClick = onClick) + .padding(vertical = 10.dp), + style = HapHapTheme.typography.body.sb14, + color = textColor, + ) +} + +@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF) +@Composable +private fun SettingMenuItemPreview() { + HapHapTheme { + SettingMenuItem(text = "계정 정보") + } +} diff --git a/app/src/main/java/com/haphap/app/presentation/setting/component/SettingSectionDivider.kt b/app/src/main/java/com/haphap/app/presentation/setting/component/SettingSectionDivider.kt new file mode 100644 index 000000000..3ce633005 --- /dev/null +++ b/app/src/main/java/com/haphap/app/presentation/setting/component/SettingSectionDivider.kt @@ -0,0 +1,33 @@ +package com.haphap.app.presentation.setting.component + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +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.haphap.app.core.designsystem.theme.HapHapTheme + +@Composable +fun SettingSectionDivider(modifier: Modifier = Modifier) { + Column(modifier = modifier) { + Spacer(modifier = Modifier.height(17.dp)) + + HorizontalDivider( + color = HapHapTheme.colors.gray200, + thickness = 1.dp, + ) + + Spacer(modifier = Modifier.height(16.dp)) + } +} + +@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF) +@Composable +private fun SettingSectionDividerPreview() { + HapHapTheme { + SettingSectionDivider() + } +} diff --git a/app/src/main/java/com/haphap/app/presentation/setting/component/SettingSectionTitle.kt b/app/src/main/java/com/haphap/app/presentation/setting/component/SettingSectionTitle.kt new file mode 100644 index 000000000..72d0ddea8 --- /dev/null +++ b/app/src/main/java/com/haphap/app/presentation/setting/component/SettingSectionTitle.kt @@ -0,0 +1,28 @@ +package com.haphap.app.presentation.setting.component + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.haphap.app.core.designsystem.theme.HapHapTheme + +@Composable +fun SettingSectionTitle( + text: String, + modifier: Modifier = Modifier, +) { + Text( + text = text, + modifier = modifier, + style = HapHapTheme.typography.caption.m12, + color = HapHapTheme.colors.gray400, + ) +} + +@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF) +@Composable +private fun SettingSectionTitlePreview() { + HapHapTheme { + SettingSectionTitle(text = "계정") + } +} diff --git a/app/src/main/java/com/haphap/app/presentation/setting/component/SettingTopBar.kt b/app/src/main/java/com/haphap/app/presentation/setting/component/SettingTopBar.kt new file mode 100644 index 000000000..3054a2772 --- /dev/null +++ b/app/src/main/java/com/haphap/app/presentation/setting/component/SettingTopBar.kt @@ -0,0 +1,59 @@ +package com.haphap.app.presentation.setting.component + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +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.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.haphap.app.R +import com.haphap.app.core.designsystem.theme.HapHapTheme +import com.haphap.app.core.extensions.noRippleClickable + +@Composable +fun SettingTopBar( + onBackClick: () -> Unit, + modifier: Modifier = Modifier, + isText: Boolean = true, +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_back_30), + contentDescription = null, + tint = Color.Unspecified, + modifier = Modifier.noRippleClickable(onClick = onBackClick), + ) + + Spacer(modifier = Modifier.width(10.dp)) + + if (isText) Text( + text = "설정", + style = HapHapTheme.typography.subtitle.b20, + color = HapHapTheme.colors.gray800, + ) + } +} + +@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF) +@Composable +private fun SettingTopBarPreview() { + HapHapTheme { + SettingTopBar( + onBackClick = {}, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/haphap/app/presentation/setting/navigation/SettingNavigation.kt b/app/src/main/java/com/haphap/app/presentation/setting/navigation/SettingNavigation.kt new file mode 100644 index 000000000..a59def0a3 --- /dev/null +++ b/app/src/main/java/com/haphap/app/presentation/setting/navigation/SettingNavigation.kt @@ -0,0 +1,40 @@ +package com.haphap.app.presentation.setting.navigation + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.ui.Modifier +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.haphap.app.core.extensions.clearBackStackNavOptions +import com.haphap.app.core.navigation.Route +import com.haphap.app.presentation.auth.navigation.navigateToLogin +import com.haphap.app.presentation.leave.navigation.navigateToLeave +import com.haphap.app.presentation.setting.SettingRoute +import kotlinx.serialization.Serializable + +fun NavController.navigateToSetting( + navOptions: NavOptions? = null, +) = navigate(Setting, navOptions) + +fun NavGraphBuilder.settingGraph( + innerPadding: PaddingValues, + navController: NavController, +) { + composable { + SettingRoute( + navigateBack = navController::popBackStack, + navigateToLeave = navController::navigateToLeave, + navigateToLogin = { + navController.navigateToLogin( + navOptions = navController.clearBackStackNavOptions() + ) + }, + modifier = Modifier.padding(innerPadding), + ) + } +} + +@Serializable +data object Setting : Route diff --git a/app/src/main/res/drawable/ic_setting_32.xml b/app/src/main/res/drawable/ic_setting_32.xml new file mode 100644 index 000000000..5cb31c8c6 --- /dev/null +++ b/app/src/main/res/drawable/ic_setting_32.xml @@ -0,0 +1,20 @@ + + + +