diff --git a/data/src/main/java/com/nexters/boolti/data/datasource/AuthDataSource.kt b/data/src/main/java/com/nexters/boolti/data/datasource/AuthDataSource.kt index aaa135a3..3c8a2c45 100644 --- a/data/src/main/java/com/nexters/boolti/data/datasource/AuthDataSource.kt +++ b/data/src/main/java/com/nexters/boolti/data/datasource/AuthDataSource.kt @@ -35,6 +35,7 @@ internal class AuthDataSource @Inject constructor( nickname = it.nickname ?: "", email = it.email ?: "", imgPath = it.photo, + userCode = it.userCode, ) } } @@ -61,6 +62,7 @@ internal class AuthDataSource @Inject constructor( email = null, phoneNumber = null, photo = null, + userCode = null, accessToken = "", refreshToken = "", ) @@ -80,6 +82,7 @@ internal class AuthDataSource @Inject constructor( nickname = user.nickname, email = user.email, photo = user.imgPath, + userCode = user.userCode, ) } } diff --git a/data/src/main/java/com/nexters/boolti/data/db/AppSettings.kt b/data/src/main/java/com/nexters/boolti/data/db/AppSettings.kt index 149ff4fd..1f488af7 100644 --- a/data/src/main/java/com/nexters/boolti/data/db/AppSettings.kt +++ b/data/src/main/java/com/nexters/boolti/data/db/AppSettings.kt @@ -15,6 +15,7 @@ internal data class AppSettings( val email: String? = null, val phoneNumber: String? = null, val photo: String? = null, + val userCode: String? = null, val accessToken: String = "", val refreshToken: String = "", val refundPolicy: List = emptyList(), diff --git a/data/src/main/java/com/nexters/boolti/data/network/response/UserResponse.kt b/data/src/main/java/com/nexters/boolti/data/network/response/UserResponse.kt index d5182e35..3ff9fdcc 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/response/UserResponse.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/response/UserResponse.kt @@ -9,6 +9,7 @@ internal data class UserResponse( val nickname: String? = null, val email: String? = null, val imgPath: String? = null, + val userCode: String? = null, ) { fun toDomain(): User { return User( @@ -16,6 +17,7 @@ internal data class UserResponse( nickname = nickname ?: "", email = email ?: "", photo = imgPath, + userCode = userCode ?: "", ) } } diff --git a/domain/src/main/java/com/nexters/boolti/domain/model/User.kt b/domain/src/main/java/com/nexters/boolti/domain/model/User.kt index c67b6dc9..a1868408 100644 --- a/domain/src/main/java/com/nexters/boolti/domain/model/User.kt +++ b/domain/src/main/java/com/nexters/boolti/domain/model/User.kt @@ -5,4 +5,5 @@ data class User( val nickname: String = "", val email: String = "", val photo: String? = null, + val userCode: String = "", ) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c4f8259f..e5abdb7a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ targetJvm = "17" kotlin = "1.9.22" android = "8.2.2" ksp = "1.9.22-1.0.16" -composeBom = "2024.04.00" +composeBom = "2024.06.00" activity-ktx = "1.8.2" lifecycle = "2.7.0" diff --git a/presentation/build.gradle.kts b/presentation/build.gradle.kts index b884ff72..5f2ff6b8 100644 --- a/presentation/build.gradle.kts +++ b/presentation/build.gradle.kts @@ -18,6 +18,7 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") buildConfigField("String", "PACKAGE_NAME", "\"${libs.versions.packageName.get()}\"") + buildConfigField("String", "VERSION_NAME", "\"${libs.versions.versionName.get()}\"") buildConfigField("String", "DEV_SUBDOMAIN", getLocalProperty("DEV_SUBDOMAIN")) } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/component/CopyButton.kt b/presentation/src/main/java/com/nexters/boolti/presentation/component/CopyButton.kt deleted file mode 100644 index 71a58bd8..00000000 --- a/presentation/src/main/java/com/nexters/boolti/presentation/component/CopyButton.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.nexters.boolti.presentation.component - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.nexters.boolti.presentation.R -import com.nexters.boolti.presentation.theme.Grey85 - -@Composable -fun CopyButton( - label: String, - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - Row( - modifier = modifier - .clip(shape = RoundedCornerShape(4.dp)) - .clickable(onClick = onClick) - .height(30.dp) - .background(color = Grey85) - .padding(horizontal = 12.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_copy), - contentDescription = label - ) - Text( - modifier = Modifier.padding(start = 6.dp), - text = label, - style = MaterialTheme.typography.labelMedium - ) - } -} \ No newline at end of file diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/component/SmallButton.kt b/presentation/src/main/java/com/nexters/boolti/presentation/component/SmallButton.kt new file mode 100644 index 00000000..57fd02fe --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/component/SmallButton.kt @@ -0,0 +1,84 @@ +package com.nexters.boolti.presentation.component + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.nexters.boolti.presentation.R +import com.nexters.boolti.presentation.theme.BooltiTheme +import com.nexters.boolti.presentation.theme.Grey05 +import com.nexters.boolti.presentation.theme.Grey50 +import com.nexters.boolti.presentation.theme.Grey80 +import com.nexters.boolti.presentation.theme.Grey85 + +@Composable +fun SmallButton( + label: String, + modifier: Modifier = Modifier, + @DrawableRes iconRes: Int? = null, + backgroundColor: Color = Grey85, + iconTint: Color = Grey50, + labelStyle: TextStyle = MaterialTheme.typography.labelMedium.copy(color = Grey05), + onClick: () -> Unit, +) { + Row( + modifier = modifier + .clip(shape = RoundedCornerShape(4.dp)) + .clickable(onClick = onClick) + .height(30.dp) + .background(color = backgroundColor) + .padding(horizontal = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + iconRes?.let { id -> + Icon( + modifier = modifier.padding(end = 6.dp), + painter = painterResource(id = id), + tint = iconTint, + contentDescription = label, + ) + } + Text( + text = label, + style = labelStyle, + ) + } +} + +@Preview +@Composable +private fun CopyButtonPreview() { + BooltiTheme { + SmallButton( + label = stringResource(R.string.ticketing_copy_address), + iconRes = R.drawable.ic_copy, + ) { } + } +} + +@Preview +@Composable +private fun LoginButtonPreview() { + BooltiTheme { + SmallButton( + label = stringResource(R.string.login), + backgroundColor = Grey80, + ) { } + } +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/extension/Flow.kt b/presentation/src/main/java/com/nexters/boolti/presentation/extension/Flow.kt new file mode 100644 index 00000000..7855661c --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/extension/Flow.kt @@ -0,0 +1,11 @@ +package com.nexters.boolti.presentation.extension + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn + +fun Flow.stateInUi( + scope: CoroutineScope, + initialValue: T +) = stateIn(scope, SharingStarted.WhileSubscribed(5000), initialValue) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt index 17c3374d..097f8b6a 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt @@ -25,6 +25,7 @@ import com.nexters.boolti.presentation.component.ToastSnackbarHost import com.nexters.boolti.presentation.extension.navigateToHome import com.nexters.boolti.presentation.screen.MainDestination.Home import com.nexters.boolti.presentation.screen.MainDestination.ShowDetail +import com.nexters.boolti.presentation.screen.accountsetting.AccountSettingScreen import com.nexters.boolti.presentation.screen.business.BusinessScreen import com.nexters.boolti.presentation.screen.gift.addGiftScreen import com.nexters.boolti.presentation.screen.giftcomplete.addGiftCompleteScreen @@ -192,6 +193,10 @@ fun MainNavigation(modifier: Modifier, onClickQrScan: (showId: String, showName: popBackStack = { navController.popBackStack(MainDestination.Gift.route, true)} ) BusinessScreen(popBackStack = navController::popBackStack) + AccountSettingScreen( + navigateTo = navController::navigateTo, + popBackStack = navController::popBackStack, + ) } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/MainDestination.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/MainDestination.kt index f5d80ba1..ba69ebca 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/MainDestination.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/MainDestination.kt @@ -18,7 +18,8 @@ sealed class MainDestination(val route: String) { ) } - data object Gift : MainDestination(route = "gift/{$showId}?salesTicketId={$salesTicketId}&ticketCount={$ticketCount}") { + data object Gift : + MainDestination(route = "gift/{$showId}?salesTicketId={$salesTicketId}&ticketCount={$ticketCount}") { val arguments = listOf( navArgument(showId) { type = NavType.StringType }, navArgument(salesTicketId) { type = NavType.StringType }, @@ -70,6 +71,7 @@ sealed class MainDestination(val route: String) { data object SignOut : MainDestination(route = "signout") data object Login : MainDestination(route = "login") data object Business : MainDestination(route = "business") + data object AccountSetting : MainDestination(route = "accountSetting") } /** diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/accountsetting/AccountSettingNavigation.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/accountsetting/AccountSettingNavigation.kt new file mode 100644 index 00000000..846722b3 --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/accountsetting/AccountSettingNavigation.kt @@ -0,0 +1,19 @@ +package com.nexters.boolti.presentation.screen.accountsetting + +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import com.nexters.boolti.presentation.screen.MainDestination + +fun NavGraphBuilder.AccountSettingScreen( + navigateTo: (String) -> Unit, + popBackStack: () -> Unit, +) { + composable( + route = MainDestination.AccountSetting.route, + ) { + AccountSettingScreen( + navigateBack = popBackStack, + onClickResign = { navigateTo(MainDestination.SignOut.route) }, + ) + } +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/accountsetting/AccountSettingScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/accountsetting/AccountSettingScreen.kt new file mode 100644 index 00000000..d682d09d --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/accountsetting/AccountSettingScreen.kt @@ -0,0 +1,270 @@ +package com.nexters.boolti.presentation.screen.accountsetting + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.nexters.boolti.presentation.R +import com.nexters.boolti.presentation.component.BTDialog +import com.nexters.boolti.presentation.component.BtBackAppBar +import com.nexters.boolti.presentation.theme.BooltiTheme +import com.nexters.boolti.presentation.theme.Grey30 +import com.nexters.boolti.presentation.theme.Grey50 +import com.nexters.boolti.presentation.theme.KakaoYellow +import com.nexters.boolti.presentation.theme.marginHorizontal + +@Composable +fun AccountSettingScreen( + modifier: Modifier = Modifier, + viewModel: AccountSettingViewModel = hiltViewModel(), + navigateBack: () -> Unit, + onClickResign: () -> Unit, +) { + val user by viewModel.user.collectAsStateWithLifecycle() + val loggedIn by viewModel.loggedIn.collectAsStateWithLifecycle() + + LaunchedEffect(loggedIn) { + if (loggedIn == false) navigateBack() + } + + AccountSettingScreen( + modifier = modifier, + userCode = user?.userCode ?: "", + onClickBack = navigateBack, + requireLogout = viewModel::logout, + onClickResign = onClickResign, + ) +} + +@Composable +fun AccountSettingScreen( + modifier: Modifier = Modifier, + userCode: String, + onClickBack: () -> Unit = {}, + requireLogout: () -> Unit = {}, + onClickResign: () -> Unit = {}, +) { + var showLogoutDialog by remember { mutableStateOf(false) } + val scrollState = rememberScrollState() + + Scaffold( + modifier = modifier, + topBar = { + BtBackAppBar( + title = stringResource(R.string.account_setting), + onClickBack = onClickBack, + ) + } + ) { innerPadding -> + Box( + modifier = modifier + .fillMaxSize() + .padding(innerPadding) + ) { + Column( + modifier = Modifier.verticalScroll(scrollState), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Section( + modifier = Modifier.padding(top = 20.dp), + ) { + Title(stringResource(R.string.user_code)) + Text( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp), + text = "#$userCode", + color = Grey30, + ) + } + + Section { + Title(stringResource(R.string.sns_provider)) + KakaoChip(modifier = Modifier.padding(top = 16.dp)) + } + + Section( + modifier = Modifier + .padding(bottom = 100.dp) + .clickable { showLogoutDialog = true }, + ) { + Row( + modifier = Modifier.fillMaxWidth(), + ) { + Title( + modifier = Modifier.weight(1f), + title = stringResource(R.string.my_logout), + ) + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_arrow_right), + contentDescription = stringResource(R.string.my_logout), + tint = Grey50, + ) + } + } + } + + TextButton( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = 30.dp), + onClick = onClickResign, + ) { + Text( + style = MaterialTheme.typography.bodyLarge, + text = stringResource(R.string.signout), + textDecoration = TextDecoration.Underline, + color = Grey50, + ) + } + + if (showLogoutDialog) { + BTDialog( + positiveButtonLabel = stringResource(id = R.string.my_logout), + onClickPositiveButton = { + showLogoutDialog = false + requireLogout() + }, + onDismiss = { showLogoutDialog = false } + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(id = R.string.my_logout_popup), + textAlign = TextAlign.Center, + ) + } + } + } + } +} + +@Composable +private fun Section( + modifier: Modifier = Modifier, + content: @Composable ColumnScope.() -> Unit, +) { + Column( + modifier = modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surface) + .padding(vertical = 16.dp, horizontal = marginHorizontal), + content = content, + ) +} + +@Composable +private fun Title( + title: String, + modifier: Modifier = Modifier, +) { + Text( + modifier = modifier, + text = title, + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onBackground, + ) +} + +@Composable +private fun KakaoChip(modifier: Modifier = Modifier) { + SnsProvider( + modifier = modifier, + iconRes = R.drawable.ic_kakaotalk, + iconBackgroundColor = KakaoYellow, + label = stringResource(R.string.kakao), + ) +} + +@Composable +private fun SnsProvider( + @DrawableRes iconRes: Int, + iconBackgroundColor: Color, + label: String, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .border(width = 1.dp, color = MaterialTheme.colorScheme.outline, shape = CircleShape) + .padding(top = 6.dp, bottom = 6.dp, start = 6.dp, end = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier + .size(32.dp) + .clip(CircleShape) + .background(iconBackgroundColor), + contentAlignment = Alignment.Center, + ) { + Image( + modifier = Modifier + .size(20.dp) + .clip(CircleShape), + imageVector = ImageVector.vectorResource(iconRes), + contentDescription = label, + ) + } + Text( + modifier = Modifier.padding(start = 12.dp), + text = label, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } +} + +@Preview +@Composable +private fun KakaoChipPreview() { + BooltiTheme { + KakaoChip() + } +} + +@Preview +@Composable +private fun AccountSettingScreenPreview() { + BooltiTheme { + AccountSettingScreen( + userCode = "AB1800028", + onClickBack = {}, + requireLogout = {}, + ) { } + } +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/accountsetting/AccountSettingViewModel.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/accountsetting/AccountSettingViewModel.kt new file mode 100644 index 00000000..cfcd36a0 --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/accountsetting/AccountSettingViewModel.kt @@ -0,0 +1,26 @@ +package com.nexters.boolti.presentation.screen.accountsetting + +import androidx.lifecycle.viewModelScope +import com.nexters.boolti.domain.repository.AuthRepository +import com.nexters.boolti.presentation.base.BaseViewModel +import com.nexters.boolti.presentation.extension.stateInUi +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class AccountSettingViewModel @Inject constructor( + private val repository: AuthRepository, +) : BaseViewModel() { + val user = repository.cachedUser + .stateInUi(viewModelScope, null) + + val loggedIn = repository.loggedIn + .stateInUi(viewModelScope, null) + + fun logout() { + viewModelScope.launch { + repository.logout() + } + } +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeNavigation.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeNavigation.kt index 15ef653d..f74ddc66 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeNavigation.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeNavigation.kt @@ -17,10 +17,10 @@ fun NavGraphBuilder.HomeScreen( onClickShowItem = { navigateTo("${MainDestination.ShowDetail.route}/$it") }, onClickTicket = { navigateTo("${MainDestination.TicketDetail.route}/$it") }, onClickQrScan = { navigateTo(MainDestination.HostedShows.route) }, - onClickSignout = { navigateTo(MainDestination.SignOut.route) }, + onClickAccountSetting = { navigateTo(MainDestination.AccountSetting.route) }, navigateToReservations = { navigateTo(MainDestination.Reservations.route) }, navigateToBusiness = { navigateTo(MainDestination.Business.route) }, - requireLogin = { navigateTo(MainDestination.Login.route) } + requireLogin = { navigateTo(MainDestination.Login.route) }, ) } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt index 071d80f9..87eb1034 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt @@ -46,7 +46,7 @@ fun HomeScreen( onClickShowItem: (showId: String) -> Unit, onClickTicket: (ticketId: String) -> Unit, onClickQrScan: () -> Unit, - onClickSignout: () -> Unit, + onClickAccountSetting: () -> Unit, navigateToReservations: () -> Unit, navigateToBusiness: () -> Unit, requireLogin: () -> Unit, @@ -123,9 +123,9 @@ fun HomeScreen( MyScreen( modifier = modifier.padding(innerPadding), requireLogin = requireLogin, + onClickAccountSetting = onClickAccountSetting, navigateToReservations = navigateToReservations, onClickQrScan = onClickQrScan, - onClickSignout = onClickSignout, ) } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/my/MyScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/my/MyScreen.kt index 9b31bcfa..e5c6adeb 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/my/MyScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/my/MyScreen.kt @@ -1,206 +1,321 @@ package com.nexters.boolti.presentation.screen.my +import androidx.annotation.DrawableRes import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect 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.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.compose.AsyncImage import com.nexters.boolti.domain.model.User +import com.nexters.boolti.presentation.BuildConfig import com.nexters.boolti.presentation.R -import com.nexters.boolti.presentation.component.BTDialog -import com.nexters.boolti.presentation.theme.Grey10 +import com.nexters.boolti.presentation.component.SmallButton +import com.nexters.boolti.presentation.theme.BooltiTheme import com.nexters.boolti.presentation.theme.Grey30 -import com.nexters.boolti.presentation.theme.Grey50 +import com.nexters.boolti.presentation.theme.Grey80 import com.nexters.boolti.presentation.theme.marginHorizontal +import com.nexters.boolti.presentation.theme.point3 @Composable fun MyScreen( + modifier: Modifier = Modifier, + viewModel: MyViewModel = hiltViewModel(), requireLogin: () -> Unit, + onClickAccountSetting: () -> Unit, navigateToReservations: () -> Unit, onClickQrScan: () -> Unit, - onClickSignout: () -> Unit, - modifier: Modifier = Modifier, - viewModel: MyViewModel = hiltViewModel(), ) { val user by viewModel.user.collectAsStateWithLifecycle() - var openLogoutDialog by remember { mutableStateOf(false) } val uriHandler = LocalUriHandler.current LaunchedEffect(Unit) { viewModel.fetchMyInfo() } - Column( - modifier = modifier.verticalScroll(state = rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - MyHeader(user = user, requireLogin = requireLogin) - MyButton( - modifier = Modifier.fillMaxWidth(), - text = stringResource(id = R.string.my_ticketing_history), - onClick = if (user == null) requireLogin else navigateToReservations, - ) - MyButton( - modifier = Modifier - .fillMaxWidth() - .padding(top = 12.dp), - text = stringResource(R.string.my_register_show), - onClick = { - if (user != null) { - uriHandler.openUri("https://boolti.in/home") // 웹에서 로그인되지 않은 상태라면 login 페이지로 리다이렉션 시킴 - } else { - uriHandler.openUri("https://boolti.in/login") - } - } - ) - MyButton( - modifier = Modifier - .fillMaxWidth() - .padding(top = 12.dp), - text = stringResource(id = R.string.my_scan_qr), - onClick = onClickQrScan, - ) - - if (user != null) { - MyButton( - modifier = Modifier - .fillMaxWidth() - .padding(top = 12.dp), - text = stringResource(id = R.string.my_logout), - onClick = { openLogoutDialog = true }, - ) - } + MyScreen( + modifier = modifier, + user = user, + onClickHeaderButton = if (user != null) requireLogin else requireLogin, // TODO 프로필 구현 후 프로필 화면 이동 연결 + onClickAccountSetting = if (user != null) onClickAccountSetting else requireLogin, + onClickReservations = if (user != null) navigateToReservations else requireLogin, + onClickRegisterShow = { + val url = if (user != null) "https://boolti.in/home" else "https://boolti.in/login" + uriHandler.openUri(url) + }, + onClickQrScan = if (user != null) onClickQrScan else requireLogin, + ) +} - Spacer(modifier = Modifier.weight(1.0f)) - if (user != null) SignoutButton(onClick = onClickSignout) - } +@Composable +fun MyScreen( + modifier: Modifier = Modifier, + user: User? = null, + onClickHeaderButton: () -> Unit = {}, + onClickAccountSetting: () -> Unit = {}, + onClickReservations: () -> Unit = {}, + onClickRegisterShow: () -> Unit = { }, + onClickQrScan: () -> Unit = {}, +) { + val scrollState = rememberScrollState() - if (openLogoutDialog) { - BTDialog( - positiveButtonLabel = stringResource(id = R.string.my_logout), - onClickPositiveButton = { - openLogoutDialog = false - viewModel.logout() - }, - onDismiss = { openLogoutDialog = false } + Surface { + Box( + modifier = modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background), ) { - Text( - modifier = Modifier.fillMaxWidth(), - text = stringResource(id = R.string.my_logout_popup), - textAlign = TextAlign.Center, - ) + Column { + MyHeader( + modifier = Modifier.zIndex(1f), + user = user, + onClickButton = onClickHeaderButton, + ) + Column( + modifier = Modifier + .offset(y = (-12).dp) + .verticalScroll(scrollState), + ) { + MyMenu( + modifier = Modifier.padding(top = 32.dp), + iconRes = R.drawable.ic_profile, + label = stringResource(R.string.account_setting), + onClick = onClickAccountSetting, + ) + MyMenu( + iconRes = R.drawable.ic_list, + label = stringResource(R.string.my_ticketing_history), + onClick = onClickReservations + ) + + HorizontalDivider( + modifier = Modifier.padding(vertical = 32.dp, horizontal = marginHorizontal), + color = MaterialTheme.colorScheme.surfaceTint, + ) + + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = marginHorizontal), + text = stringResource(R.string.my_show), + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onBackground, + ) + + MyMenu( + modifier = Modifier.padding(top = 8.dp), + iconRes = R.drawable.ic_plus_ticket, + label = stringResource(R.string.my_register_show), + onClick = onClickRegisterShow, + ) + MyMenu( + iconRes = R.drawable.ic_qr_simple, + label = stringResource(R.string.my_scan_qr), + onClick = onClickQrScan, + ) + } + } + Column( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = 36.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + modifier = Modifier.size(width = 79.dp, height = 28.dp), + imageVector = ImageVector.vectorResource(R.drawable.ic_logo_boolti), + tint = Grey80, + contentDescription = stringResource(R.string.description_app_logo), + ) + Text( + modifier = Modifier.padding(top = 8.dp), + text = "Version ${BuildConfig.VERSION_NAME}", + style = MaterialTheme.typography.bodySmall, + color = Grey80, + ) + } } } } @Composable -fun MyHeader(modifier: Modifier = Modifier, user: User?, requireLogin: () -> Unit) { - val headerModifier = if (user == null) modifier.clickable(onClick = requireLogin) else modifier - +private fun MyHeader( + modifier: Modifier = Modifier, + user: User? = null, + onClickButton: () -> Unit, +) { + val shape = RoundedCornerShape( + bottomStart = 12.dp, + bottomEnd = 12.dp, + ) Row( - modifier = headerModifier + modifier = modifier .fillMaxWidth() - .padding(horizontal = marginHorizontal) - .padding(top = 40.dp, bottom = 32.dp), + .clip(shape) + .background(MaterialTheme.colorScheme.surface) + .padding(vertical = 28.dp, horizontal = marginHorizontal), verticalAlignment = Alignment.CenterVertically, ) { - AsyncImage( - modifier = Modifier - .size(70.dp) - .clip(shape = RoundedCornerShape(100.dp)), - model = user?.photo, - contentDescription = null, - fallback = painterResource(id = R.drawable.ic_fallback_profile) - ) - Column( - modifier = Modifier.padding(start = 12.dp), - verticalArrangement = Arrangement.Center, - ) { - Text( - text = user?.nickname ?: stringResource(id = R.string.my_login), - style = MaterialTheme.typography.titleLarge - ) - Text( - text = user?.email ?: stringResource(id = R.string.my_login_sub), - style = MaterialTheme.typography.bodyLarge.copy(color = Grey30), + user?.let { + AsyncImage( + modifier = Modifier + .padding(end = 12.dp) + .size(36.dp) + .clip(shape = CircleShape) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.outline, + shape = CircleShape, + ), + model = user.photo, + contentDescription = null, + placeholder = painterResource(id = R.drawable.ic_fallback_profile), + fallback = painterResource(id = R.drawable.ic_fallback_profile), ) } - Spacer(modifier = modifier.weight(1.0f)) - if (user == null) { - Icon( - modifier = Modifier.padding(start = 12.dp), - painter = painterResource(id = R.drawable.ic_arrow_right), - contentDescription = null, - tint = Grey50, + Text( + modifier = Modifier.weight(1f), + text = user?.nickname ?: stringResource(R.string.my_login), + style = point3, + fontWeight = FontWeight.Normal, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + if (user == null) { // TODO 프로필 기능 추가 후 조건문 제거 + SmallButton( + modifier = Modifier.padding(start = 16.dp), + label = if (user != null) { + stringResource(R.string.show_profile_button) + } else { + stringResource(R.string.login) + }, + backgroundColor = Grey80, + onClick = onClickButton, ) } } } @Composable -private fun MyButton( - text: String, - onClick: () -> Unit, +private fun MyMenu( + @DrawableRes iconRes: Int, + label: String, modifier: Modifier = Modifier, + onClick: () -> Unit, ) { Row( modifier = modifier - .clickable(onClick = onClick) - .background(color = MaterialTheme.colorScheme.surface) - .padding(horizontal = marginHorizontal, vertical = 20.dp), - horizontalArrangement = Arrangement.SpaceBetween, + .fillMaxWidth() + .clip(RoundedCornerShape(4.dp)) + .clickable( + role = Role.Button, + onClick = onClick, + ) + .padding(vertical = 12.dp, horizontal = marginHorizontal), ) { + Icon( + modifier = Modifier.size(24.dp), + imageVector = ImageVector.vectorResource(iconRes), + tint = Grey30, + contentDescription = label, + ) Text( - text = text, - style = MaterialTheme.typography.titleLarge.copy(color = Grey10), + modifier = Modifier.padding(start = 12.dp), + text = label, + style = MaterialTheme.typography.bodyLarge, + color = Grey30, ) - Icon( - painter = painterResource(id = R.drawable.ic_arrow_right), - contentDescription = null, - tint = Grey50, + } +} + +@Preview("로그인 한 유저 헤더") +@Composable +private fun MyHeaderUserPreview() { + val user = User( + id = "", + nickname = "일이삼사오육칠팔구십", + email = "boolti@gmail.com", + photo = "https://images.unsplash.com/photo-1721497684662-cf36f0ee232e?q=80&w=4965&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", + userCode = "AB1800028", + ) + BooltiTheme { + MyHeader(user = user) {} + } +} + +@Preview("게스트 헤더") +@Composable +private fun MyHeaderGuestPreview() { + BooltiTheme { + MyHeader(user = null) {} + } +} + +@Preview +@Composable +private fun MyScreenPreview() { + val user = User( + id = "", + nickname = "불티유저", + email = "boolti@gmail.com", + photo = "https://images.unsplash.com/photo-1721497684662-cf36f0ee232e?q=80&w=4965&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", + userCode = "AB1800028", + ) + BooltiTheme { + MyScreen( + user = user, ) } } +@Preview(device = "spec:parent=pixel_5,orientation=landscape") @Composable -fun SignoutButton( - modifier: Modifier = Modifier, - onClick: () -> Unit, -) { - Text( - modifier = modifier - .padding(bottom = 40.dp) - .clickable(onClick = onClick), - text = stringResource(id = R.string.signout), - style = MaterialTheme.typography.bodySmall.copy(color = Grey50), - textDecoration = TextDecoration.Underline, +private fun MyScreenLandscapePreview() { + val user = User( + id = "", + nickname = "불티유저", + email = "boolti@gmail.com", + photo = "https://images.unsplash.com/photo-1721497684662-cf36f0ee232e?q=80&w=4965&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", + userCode = "AB1800028", ) + BooltiTheme { + MyScreen( + user = user, + ) + } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/showdetail/ShowDetailScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/showdetail/ShowDetailScreen.kt index 0ba869a8..9430c4b3 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/showdetail/ShowDetailScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/showdetail/ShowDetailScreen.kt @@ -64,10 +64,10 @@ import com.nexters.boolti.domain.model.ShowState import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.component.BtAppBar import com.nexters.boolti.presentation.component.BtAppBarDefaults -import com.nexters.boolti.presentation.component.CopyButton import com.nexters.boolti.presentation.component.MainButton import com.nexters.boolti.presentation.component.MainButtonDefaults import com.nexters.boolti.presentation.component.ShowInquiry +import com.nexters.boolti.presentation.component.SmallButton import com.nexters.boolti.presentation.extension.requireActivity import com.nexters.boolti.presentation.screen.LocalSnackbarController import com.nexters.boolti.presentation.screen.ticketing.ChooseTicketBottomSheet @@ -360,7 +360,8 @@ private fun ContentScaffold( val clipboardManager = LocalClipboardManager.current val copiedMessage = stringResource(id = R.string.ticketing_address_copied_message) - CopyButton( + SmallButton( + iconRes = R.drawable.ic_copy, label = stringResource(id = R.string.ticketing_copy_address), onClick = { clipboardManager.setText(AnnotatedString(showDetail.streetAddress)) diff --git a/presentation/src/main/res/drawable/ic_list.xml b/presentation/src/main/res/drawable/ic_list.xml new file mode 100644 index 00000000..e80a2637 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_list.xml @@ -0,0 +1,38 @@ + + + + + + + + + diff --git a/presentation/src/main/res/drawable/ic_plus_ticket.xml b/presentation/src/main/res/drawable/ic_plus_ticket.xml new file mode 100644 index 00000000..5ed2f83f --- /dev/null +++ b/presentation/src/main/res/drawable/ic_plus_ticket.xml @@ -0,0 +1,31 @@ + + + + + + + + diff --git a/presentation/src/main/res/drawable/ic_profile.xml b/presentation/src/main/res/drawable/ic_profile.xml new file mode 100644 index 00000000..5d0362f1 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_profile.xml @@ -0,0 +1,31 @@ + + + + + + + + diff --git a/presentation/src/main/res/drawable/ic_qr_simple.xml b/presentation/src/main/res/drawable/ic_qr_simple.xml new file mode 100644 index 00000000..7b113d09 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_qr_simple.xml @@ -0,0 +1,41 @@ + + + + + + + diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 26be2cd3..36d9d5e2 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -20,6 +20,8 @@ 이 공연의 티켓이 아니에요 존재하지 않는 티켓이에요 + 카카오 + 이름을 올바르게 입력해 주세요 연락처를 올바르게 입력해 주세요 @@ -50,6 +52,7 @@ 확인 로그인 하러 가기 + 로그인 알 수 없는 에러가 발생했습니다 복사 다음 @@ -72,21 +75,20 @@ 약관 동의하고 시작하기 불티를 찾아주셔서 감사합니다 불티 유저 - 탈퇴 후 30일 이내에 로그인하여, 계정 삭제가 취소되었어요\n불티를 다시 찾아주셔서 감사해요! + 30일 내에 로그인하여 계정 삭제가 취소되었어요.\n불티를 다시 찾아주셔서 감사해요! + 식별 코드 - 회원 탈퇴 - 탈퇴하기 - 탈퇴하시겠어요? - 탈퇴일로부터 30일 이내로 로그인 시 계정 삭제를 취소할 수 있습니다. 30일이 지나면 계정 및 정보가 영구 삭제됩니다. - 탈퇴 전, 꼭 읽어보세요! + 계정 삭제 + 삭제하기 + 삭제 전, 꼭 읽어보세요! 주최한 공연 정보는 사라지지 않아요 예매한 티켓은 전부 사라지며 복구할 수 없어요 - 탈퇴 일로부터 30일 이내 재 로그인 시 계정 삭제를 취소할 수 있어요 + 삭제일로부터 30일 이내 재 로그인 시 계정 삭제를 취소할 수 있어요 - 탈퇴 이유를 입력해주세요 - 예) 계정 탈퇴 후 재가입할게요 + 삭제 이유를 입력해주세요 + 예) 계정 삭제 후 재가입할게요 %,d원 총 %,d원 @@ -213,13 +215,17 @@ 예매 진행 중 오류가 발생하였습니다.\n다시 시도해 주세요 - 불티 로그인 하러가기 + 로그인하고 이용해보세요 원하는 공연 티켓을 예매해보세요! 로그아웃 결제 내역 - QR 스캔 + 입장 확인 정말 로그아웃 하시겠어요? 공연 등록 + 프로필 보기 + 계정 설정 + 내 공연 + 연결 서비스 QR 스캔