diff --git a/README.md b/README.md index 8c0a14fb..6a17e90f 100644 --- a/README.md +++ b/README.md @@ -17,4 +17,12 @@ ## 기능 목록 - [O] 카드사 목록 화면 구현 - [O] 선택한 카드사에 따라 카드 미리보기가 바뀌어야 한다. -- [] 카드사를 선택할 때 적절한 카드사 아이콘을 노출한다. +- [O] 카드사를 선택할 때 적절한 카드사 아이콘을 노출한다. +- [O] 카드 등록 시 카드에 카드사 이름이 표시된다. + +# Step 4 - 페이먼츠(카드 수정) + +## 기능 목록 +- [O] 등록된 카드 선택시 카드 수정 화면으로 이동한다. +- [O] 카드 수정 시 변경이 일어나지 않으면 수정이 불가능하다. +- [O] 카드가 수정되면 카드 목록 화면에 변경사항이 반영된다. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9996218f..a229b1ba 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,6 +1,7 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.jetbrains.kotlin.android) + id("kotlin-parcelize") } android { diff --git a/app/src/androidTest/java/nextstep/payments/screen/card/list/RegisteredCreditCardsScreenTest.kt b/app/src/androidTest/java/nextstep/payments/screen/card/list/RegisteredCreditCardsScreenTest.kt index a0f45e00..e1f4da93 100644 --- a/app/src/androidTest/java/nextstep/payments/screen/card/list/RegisteredCreditCardsScreenTest.kt +++ b/app/src/androidTest/java/nextstep/payments/screen/card/list/RegisteredCreditCardsScreenTest.kt @@ -8,6 +8,7 @@ import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.test.core.app.ApplicationProvider import nextstep.payments.R +import nextstep.payments.data.BankType import nextstep.payments.data.Card import nextstep.payments.data.PaymentCardsRepository import nextstep.payments.data.RegisteredCreditCards @@ -50,18 +51,20 @@ class RegisteredCreditCardsScreenTest { val registeredCreditCards = RegisteredCreditCards( mutableListOf( Card( + id = 1, cardNumber = "1234-5678-1234-5678", ownerName = "홍길동", expiredDate = "12/24", password = "123", - brandColor = Color(context.getColor(R.color.bc_card)) + bankType = BankType.BC ), Card( + id = 2, cardNumber = "1234-5678-1234-5628", ownerName = "홍길동", expiredDate = "12/24", password = "123", - brandColor = Color(context.getColor(R.color.bc_card)) + bankType = BankType.BC ) ) ) @@ -119,21 +122,23 @@ class RegisteredCreditCardsScreenTest { // given : 카드 등록이 되어있다. PaymentCardsRepository.addCard( Card( + id = 13, cardNumber = "1234-5678-1234-5628", ownerName = "홍길동", expiredDate = "12/24", password = "123", - brandColor = Color(context.getColor(R.color.bc_card)) + bankType = BankType.BC ) ) val registeredCreditCards = RegisteredCreditCards( mutableListOf( Card( + id = 4, cardNumber = "1234-5678-1234-5628", ownerName = "홍길동", expiredDate = "12/24", password = "123", - brandColor = Color(context.getColor(R.color.bc_card)) + bankType = BankType.BC ) ) ) diff --git a/app/src/main/java/nextstep/payments/MainActivity.kt b/app/src/main/java/nextstep/payments/MainActivity.kt index 6656f923..64935c8a 100644 --- a/app/src/main/java/nextstep/payments/MainActivity.kt +++ b/app/src/main/java/nextstep/payments/MainActivity.kt @@ -17,11 +17,11 @@ import nextstep.payments.ui.card.registration.NewCardActivity import nextstep.payments.ui.theme.PaymentsTheme class MainActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val viewModel by viewModels() + private val viewModel: CardListViewModel by viewModels { CardListViewModel.Factory } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) setContent { val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { @@ -40,12 +40,28 @@ class MainActivity : ComponentActivity() { onAddCard = { val intent = Intent(this, NewCardActivity::class.java) launcher.launch(intent) + }, + onCardClick = { card -> + val intent = Intent(this, NewCardActivity::class.java).apply { + putExtra("card", card) + } + launcher.launch(intent) } ) } } } } + + override fun onPause() { + super.onPause() + viewModel.saveCard() + } + + override fun onResume() { + super.onResume() + viewModel.openCard() + } } diff --git a/app/src/main/java/nextstep/payments/NewCardViewModel.kt b/app/src/main/java/nextstep/payments/NewCardViewModel.kt deleted file mode 100644 index 4b928d55..00000000 --- a/app/src/main/java/nextstep/payments/NewCardViewModel.kt +++ /dev/null @@ -1,51 +0,0 @@ -package nextstep.payments - -import androidx.lifecycle.ViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import nextstep.payments.data.BankType -import nextstep.payments.data.Card -import nextstep.payments.data.PaymentCardsRepository - -class NewCardViewModel( - private val repository: PaymentCardsRepository = PaymentCardsRepository -) : ViewModel() { - - private val _cardNumber = MutableStateFlow("") - val cardNumber: StateFlow = _cardNumber.asStateFlow() - - private val _expiredDate = MutableStateFlow("") - val expiredDate: StateFlow = _expiredDate.asStateFlow() - - private val _ownerName = MutableStateFlow("") - val ownerName: StateFlow = _ownerName.asStateFlow() - - private val _password = MutableStateFlow("") - val password: StateFlow = _password.asStateFlow() - - private val _cardAdded = MutableStateFlow(false) - val cardAdded: StateFlow = _cardAdded.asStateFlow() - - - fun setCardNumber(cardNumber: String) { - _cardNumber.value = cardNumber - } - - fun setExpiredDate(expiredDate: String) { - _expiredDate.value = expiredDate - } - - fun setOwnerName(ownerName: String) { - _ownerName.value = ownerName - } - - fun setPassword(password: String) { - _password.value = password - } - - fun addCard(card: Card) { - repository.addCard(card) - _cardAdded.value = true - } -} diff --git a/app/src/main/java/nextstep/payments/data/BankType.kt b/app/src/main/java/nextstep/payments/data/BankType.kt index 012fcabb..033e970a 100644 --- a/app/src/main/java/nextstep/payments/data/BankType.kt +++ b/app/src/main/java/nextstep/payments/data/BankType.kt @@ -1,12 +1,13 @@ package nextstep.payments.data import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes import androidx.annotation.StringRes import nextstep.payments.R enum class BankType( @StringRes val companyName: Int, - val logo: Int, + @DrawableRes val logo: Int, @ColorRes val brandColor: Int, ) { NOT_SELECTED( diff --git a/app/src/main/java/nextstep/payments/data/Card.kt b/app/src/main/java/nextstep/payments/data/Card.kt index 997d5d0f..815356a8 100644 --- a/app/src/main/java/nextstep/payments/data/Card.kt +++ b/app/src/main/java/nextstep/payments/data/Card.kt @@ -1,11 +1,14 @@ package nextstep.payments.data -import androidx.compose.ui.graphics.Color +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +@Parcelize data class Card( + var id: Int = 0, val cardNumber: String, val expiredDate: String, val ownerName: String, val password: String, - val brandColor: Color -) + val bankType: BankType +) : Parcelable diff --git a/app/src/main/java/nextstep/payments/data/PaymentCardsRepository.kt b/app/src/main/java/nextstep/payments/data/PaymentCardsRepository.kt index a0e8a0e9..01fe9e8f 100644 --- a/app/src/main/java/nextstep/payments/data/PaymentCardsRepository.kt +++ b/app/src/main/java/nextstep/payments/data/PaymentCardsRepository.kt @@ -3,13 +3,23 @@ package nextstep.payments.data object PaymentCardsRepository { private val _cards = mutableListOf() - val cards: List get() = _cards.toList() + val cards: List get() = _cards fun addCard(card: Card) { + if (card.id == 0) card.id = createId() _cards.add(card) } fun removeAllCard() { _cards.clear() } + + fun editCard(newCard: Card) { + val index = _cards.indexOfFirst { it.id == newCard.id } + _cards[index] = newCard + } + + private fun createId(): Int { + return _cards.maxOfOrNull { it.id }?.plus(1) ?: 1 + } } diff --git a/app/src/main/java/nextstep/payments/data/RegisteredCreditCards.kt b/app/src/main/java/nextstep/payments/data/RegisteredCreditCards.kt index fb8588ef..cfa51384 100644 --- a/app/src/main/java/nextstep/payments/data/RegisteredCreditCards.kt +++ b/app/src/main/java/nextstep/payments/data/RegisteredCreditCards.kt @@ -1,8 +1,11 @@ package nextstep.payments.data +import android.os.Parcelable +import kotlinx.parcelize.Parcelize import nextstep.payments.ui.card.CreditCardUiState -data class RegisteredCreditCards(val cardList: List) { +@Parcelize +data class RegisteredCreditCards(val cardList: List) : Parcelable { fun getState(): CreditCardUiState { return when (cardList.size) { diff --git a/app/src/main/java/nextstep/payments/ui/PaymentCard.kt b/app/src/main/java/nextstep/payments/ui/PaymentCard.kt index 9f9efa16..4b0e1de0 100644 --- a/app/src/main/java/nextstep/payments/ui/PaymentCard.kt +++ b/app/src/main/java/nextstep/payments/ui/PaymentCard.kt @@ -1,12 +1,14 @@ package nextstep.payments.ui import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -26,12 +28,14 @@ import nextstep.payments.ui.card.list.component.card.CardOwnerName @Composable fun PaymentCard( + brandColor: Color, modifier: Modifier = Modifier, - brandColor: Color = Color(0xFF333333) + content: @Composable () -> Unit = {} ) { Box( contentAlignment = Alignment.CenterStart, modifier = modifier + .padding(top = 10.dp) .shadow(8.dp) .size(width = 208.dp, height = 124.dp) .background( @@ -49,19 +53,26 @@ fun PaymentCard( shape = RoundedCornerShape(4.dp), ) ) + content() } } @Composable -fun PaymentCard( +fun PaymentCardContents( card: Card, modifier: Modifier = Modifier, - content: @Composable () -> Unit = {} + onClick: (Card) -> Unit = {} ) { Box( - modifier = Modifier.padding(top = 10.dp) + modifier = Modifier + .clickable { onClick(card) } ) { - content() + Text( + text = card.bankType.name, + color = Color.White, + modifier = Modifier + .padding(start = 14.dp, top = 10.dp) + ) CardNumber( cardNumber = card.cardNumber, @@ -90,24 +101,20 @@ fun PaymentCard( @Preview @Composable -private fun PaymentCardPreview() { - PaymentCard( - brandColor = colorResource(id = BankType.BC.brandColor) +private fun NewPaymentCardPreview() { + val card = Card( + id = 1, + cardNumber = "1234-5678-1234-5678", + ownerName = "홍길동", + expiredDate = "12/34", + password = "123", + bankType = BankType.BC ) -} -@Preview -@Composable -private fun NewPaymentCardPreview() { PaymentCard( - card = Card( - cardNumber = "1234-5678-1234-5678", - ownerName = "홍길동", - expiredDate = "12/34", - password = "123", - brandColor = colorResource(id = BankType.BC.brandColor) - ), - modifier = Modifier.size(width = 208.dp, height = 124.dp), - content = { PaymentCard() } + brandColor = colorResource(id = BankType.BC.brandColor), + content = { + PaymentCardContents(card = card) + } ) } diff --git a/app/src/main/java/nextstep/payments/ui/card/list/CardListScreen.kt b/app/src/main/java/nextstep/payments/ui/card/list/CardListScreen.kt index b3ffcb92..60ec6df1 100644 --- a/app/src/main/java/nextstep/payments/ui/card/list/CardListScreen.kt +++ b/app/src/main/java/nextstep/payments/ui/card/list/CardListScreen.kt @@ -22,6 +22,8 @@ import nextstep.payments.R import nextstep.payments.data.BankType import nextstep.payments.data.Card import nextstep.payments.data.RegisteredCreditCards +import nextstep.payments.ui.PaymentCard +import nextstep.payments.ui.PaymentCardContents import nextstep.payments.ui.card.CreditCardUiState import nextstep.payments.ui.card.list.component.card.CardLazyColumn import nextstep.payments.ui.card.list.component.card.CardListTopBar @@ -33,19 +35,22 @@ import nextstep.payments.ui.theme.PaymentsTheme fun CardListScreen( viewModel: CardListViewModel = viewModel(), onAddCard: () -> Unit = {}, + onCardClick: (Card) -> Unit = {} ) { val cards by viewModel.registeredCreditCards.collectAsStateWithLifecycle() CardListScreen( registeredCreditCards = cards, onAddCard = onAddCard, + onCardClick = onCardClick ) } @Composable fun CardListScreen( registeredCreditCards: RegisteredCreditCards = RegisteredCreditCards(mutableListOf()), - onAddCard: () -> Unit = {} + onAddCard: () -> Unit = {}, + onCardClick: (Card) -> Unit = {} ) { when (registeredCreditCards.getState()) { @@ -60,6 +65,7 @@ fun CardListScreen( CardListScreenOne( cards = registeredCreditCards.cardList, onAddCard = onAddCard, + onCardClick = onCardClick ) } @@ -67,6 +73,7 @@ fun CardListScreen( CardListScreenMany( cards = registeredCreditCards.cardList, onAddCard = onAddCard, + onCardClick = onCardClick ) } } @@ -106,7 +113,8 @@ fun CardListScreenEmpty( @Composable fun CardListScreenOne( cards: List, - onAddCard: () -> Unit = {} + onAddCard: () -> Unit = {}, + onCardClick: (Card) -> Unit = {} ) { Scaffold(topBar = { CardListTopBar() }) { paddingValues -> Column( @@ -114,9 +122,15 @@ fun CardListScreenOne( .padding(paddingValues) .fillMaxWidth() ) { - CardLazyColumn( - cards = cards, - modifier = Modifier.align(CenterHorizontally) + PaymentCard( + brandColor = colorResource(id = cards.first().bankType.brandColor), + modifier = Modifier.align(CenterHorizontally), + content = { + PaymentCardContents( + card = cards.first(), + onClick = onCardClick + ) + } ) EmptyCardImage( @@ -136,7 +150,8 @@ fun CardListScreenOne( @Composable fun CardListScreenMany( cards: List, - onAddCard: () -> Unit = {} + onAddCard: () -> Unit = {}, + onCardClick: (Card) -> Unit = {} ) { Scaffold(topBar = { CardListTopBarWithAdd( @@ -150,7 +165,8 @@ fun CardListScreenMany( ) { CardLazyColumn( cards = cards, - modifier = Modifier.align(CenterHorizontally) + modifier = Modifier.align(CenterHorizontally), + onCardClick = onCardClick ) } } @@ -175,11 +191,12 @@ private fun CardListScreenOnePreview() { registeredCreditCards = RegisteredCreditCards( cardList = listOf( Card( + id = 1, cardNumber = "1234-5678-1234-6654", ownerName = "홍길동", expiredDate = "12/24", password = "123", - brandColor = colorResource(id = BankType.BC.brandColor) + bankType = BankType.BC ) ) ), @@ -194,18 +211,20 @@ private fun CardListScreenManyPreview() { val registeredCreditCards = RegisteredCreditCards( cardList = listOf( Card( + id = 1, cardNumber = "1234-5678-1234-6654", ownerName = "홍길동", expiredDate = "12/24", password = "123", - brandColor = colorResource(id = BankType.BC.brandColor) + bankType = BankType.KAKAO ), Card( + id = 1, cardNumber = "1234-5678-1234-1234", ownerName = "홍길동", expiredDate = "12/24", password = "123", - brandColor = colorResource(id = BankType.KAKAO.brandColor) + bankType = BankType.BC ) ) ) diff --git a/app/src/main/java/nextstep/payments/ui/card/list/CardListViewModel.kt b/app/src/main/java/nextstep/payments/ui/card/list/CardListViewModel.kt index 7ff8404d..d369cebf 100644 --- a/app/src/main/java/nextstep/payments/ui/card/list/CardListViewModel.kt +++ b/app/src/main/java/nextstep/payments/ui/card/list/CardListViewModel.kt @@ -1,18 +1,47 @@ package nextstep.payments.ui.card.list +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.createSavedStateHandle +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import nextstep.payments.data.RegisteredCreditCards import nextstep.payments.data.PaymentCardsRepository +import nextstep.payments.data.RegisteredCreditCards -class CardListViewModel : ViewModel() { +class CardListViewModel( + val savedStateHandle: SavedStateHandle, +) : ViewModel() { private val _registeredCreditCards = MutableStateFlow(RegisteredCreditCards(emptyList())) - val registeredCreditCards: StateFlow = _registeredCreditCards.asStateFlow() + val registeredCreditCards: StateFlow = + _registeredCreditCards.asStateFlow() fun fetchCards() { _registeredCreditCards.value = RegisteredCreditCards(PaymentCardsRepository.cards) } + + fun saveCard() { + savedStateHandle[KEY_REGISTERED_CREDIT_CARDS] = _registeredCreditCards.value + } + + fun openCard() { + _registeredCreditCards.value = + savedStateHandle[KEY_REGISTERED_CREDIT_CARDS] ?: RegisteredCreditCards(emptyList()) + } + + companion object { + private const val KEY_REGISTERED_CREDIT_CARDS = "registeredCreditCards" + val Factory: ViewModelProvider.Factory = viewModelFactory { + initializer { + val savedStateHandle = createSavedStateHandle() + CardListViewModel( + savedStateHandle = savedStateHandle + ) + } + } + } } diff --git a/app/src/main/java/nextstep/payments/ui/card/list/component/card/CardLazyColumn.kt b/app/src/main/java/nextstep/payments/ui/card/list/component/card/CardLazyColumn.kt index 0eeec93a..a4bc2e4a 100644 --- a/app/src/main/java/nextstep/payments/ui/card/list/component/card/CardLazyColumn.kt +++ b/app/src/main/java/nextstep/payments/ui/card/list/component/card/CardLazyColumn.kt @@ -8,9 +8,14 @@ import androidx.compose.ui.tooling.preview.Preview import nextstep.payments.data.BankType import nextstep.payments.data.Card import nextstep.payments.ui.PaymentCard +import nextstep.payments.ui.PaymentCardContents @Composable -fun CardLazyColumn(cards: List, modifier: Modifier = Modifier) { +fun CardLazyColumn( + cards: List, + modifier: Modifier = Modifier, + onCardClick: (Card) -> Unit = {} +) { LazyColumn( modifier = modifier ) { @@ -19,11 +24,12 @@ fun CardLazyColumn(cards: List, modifier: Modifier = Modifier) { key = { index -> cards[index].cardNumber } ) { PaymentCard( - card = cards[it], + brandColor = colorResource(id = cards[it].bankType.brandColor), modifier = Modifier, content = { - PaymentCard( - brandColor = cards[it].brandColor + PaymentCardContents( + card = cards[it], + onClick = onCardClick ) } ) @@ -37,11 +43,12 @@ private fun CardLazyColumnPreview() { CardLazyColumn( cards = listOf( Card( + id = 1, cardNumber = "1234-5678-1234-5678", ownerName = "홍길동", expiredDate = "12/34", password = "123", - brandColor = colorResource(id = BankType.KAKAO.brandColor) + bankType = BankType.KAKAO ) ) ) diff --git a/app/src/main/java/nextstep/payments/ui/card/list/component/card/EmptyCardImage.kt b/app/src/main/java/nextstep/payments/ui/card/list/component/card/EmptyCardImage.kt index cff42059..b4ac1fa7 100644 --- a/app/src/main/java/nextstep/payments/ui/card/list/component/card/EmptyCardImage.kt +++ b/app/src/main/java/nextstep/payments/ui/card/list/component/card/EmptyCardImage.kt @@ -9,7 +9,6 @@ import androidx.compose.material.icons.filled.Add import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier diff --git a/app/src/main/java/nextstep/payments/ui/card/registration/NewCardActivity.kt b/app/src/main/java/nextstep/payments/ui/card/registration/NewCardActivity.kt index 00013407..e6fb70b2 100644 --- a/app/src/main/java/nextstep/payments/ui/card/registration/NewCardActivity.kt +++ b/app/src/main/java/nextstep/payments/ui/card/registration/NewCardActivity.kt @@ -4,15 +4,20 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.viewModels -import nextstep.payments.NewCardViewModel +import nextstep.payments.data.Card import nextstep.payments.ui.theme.PaymentsTheme class NewCardActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - val viewModel by viewModels() + private val viewModel: NewCardViewModel by viewModels { NewCardViewModel.Factory } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + (intent?.getParcelableExtra("card") as Card?)?.let { + viewModel.setOldCard(it) + viewModel.setUiState(RegistrationUiState.EditCard) + } setContent { PaymentsTheme { NewCardScreen( @@ -28,6 +33,16 @@ class NewCardActivity : ComponentActivity() { } } } + + override fun onResume() { + super.onResume() + viewModel.openCardData() + } + + override fun onPause() { + super.onPause() + viewModel.saveCardData() + } } diff --git a/app/src/main/java/nextstep/payments/ui/card/registration/NewCardScreen.kt b/app/src/main/java/nextstep/payments/ui/card/registration/NewCardScreen.kt index 02f9a8fa..a63b9158 100644 --- a/app/src/main/java/nextstep/payments/ui/card/registration/NewCardScreen.kt +++ b/app/src/main/java/nextstep/payments/ui/card/registration/NewCardScreen.kt @@ -1,5 +1,6 @@ package nextstep.payments.ui.card.registration +import android.annotation.SuppressLint import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -16,29 +17,27 @@ 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.saveable.rememberSaveable 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.res.colorResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel -import nextstep.payments.NewCardViewModel import nextstep.payments.R import nextstep.payments.data.BankType -import nextstep.payments.data.Card import nextstep.payments.ui.PaymentCard import nextstep.payments.ui.card.registration.component.BankSelectRow import nextstep.payments.ui.card.registration.component.NewCardTopBar import nextstep.payments.ui.theme.PaymentsTheme // Stateful +@SuppressLint("StateFlowValueCalledInComposition") @OptIn(ExperimentalMaterial3Api::class) @Composable fun NewCardScreen( @@ -52,9 +51,10 @@ fun NewCardScreen( val ownerName by viewModel.ownerName.collectAsStateWithLifecycle() val password by viewModel.password.collectAsStateWithLifecycle() val cardAdded by viewModel.cardAdded.collectAsStateWithLifecycle() - val modalBottomSheetState = rememberModalBottomSheetState(confirmValueChange = { false }) - var selectedBankType by remember { mutableStateOf(BankType.NOT_SELECTED) } + val selectedBankType by viewModel.selectedBankType.collectAsStateWithLifecycle() var showCardCompanyBottomSheet by rememberSaveable { mutableStateOf(true) } + val modalBottomSheetState = rememberModalBottomSheetState(confirmValueChange = { false }) + LaunchedEffect(cardAdded) { if (cardAdded) navigateToCardList() @@ -72,9 +72,9 @@ fun NewCardScreen( onDismissRequest = { } ) { BankSelectRow( - onClick = { + onClickBankType = { showCardCompanyBottomSheet = false - selectedBankType = it + viewModel.setSelectBankType(it) } ) } @@ -86,13 +86,13 @@ fun NewCardScreen( expiredDate = expiredDate, ownerName = ownerName, password = password, - brandColor = colorResource(selectedBankType.brandColor), + bankType = selectedBankType, setCardNumber = viewModel::setCardNumber, setExpiredDatedNumber = viewModel::setExpiredDate, setOwnerNamedNumber = viewModel::setOwnerName, setPasswordNumber = viewModel::setPassword, onBackClick = onBackClick, - onSaveClick = viewModel::addCard, + onSaveClick = viewModel::saveCard, ) } @@ -104,26 +104,18 @@ private fun NewCardScreen( expiredDate: String, ownerName: String, password: String, - brandColor: Color, + bankType: BankType, setCardNumber: (String) -> Unit, setExpiredDatedNumber: (String) -> Unit, setOwnerNamedNumber: (String) -> Unit, setPasswordNumber: (String) -> Unit, onBackClick: () -> Unit = {}, - onSaveClick: (Card) -> Unit = {}, + onSaveClick: () -> Unit = {}, ) { Scaffold( topBar = { NewCardTopBar(onBackClick = onBackClick, onSaveClick = { - onSaveClick( - Card( - cardNumber = cardNumber, - expiredDate = expiredDate, - ownerName = ownerName, - password = password, - brandColor = brandColor - ) - ) + onSaveClick() }) }, modifier = modifier ) { innerPadding -> @@ -137,9 +129,10 @@ private fun NewCardScreen( Spacer(modifier = Modifier.height(14.dp)) PaymentCard( - brandColor = brandColor + brandColor = colorResource(bankType.brandColor) ) + Spacer(modifier = Modifier.height(10.dp)) OutlinedTextField( @@ -181,10 +174,13 @@ private fun NewCardScreen( @Preview @Composable private fun NewCardScreenPreview() { + val savedState = SavedStateHandle(mapOf("someIdArg" to 1)) PaymentsTheme { NewCardScreen(modifier = Modifier, navigateToCardList = {}, - viewModel = NewCardViewModel().apply { + viewModel = NewCardViewModel( + savedStateHandle = savedState + ).apply { setCardNumber("0000 - 0000 - 0000 -0000") setExpiredDate("02/26") setOwnerName("김수현") @@ -201,7 +197,7 @@ private fun StatelessNewCardScreenPreview() { expiredDate = "02/26", ownerName = "김수현", password = "1234", - brandColor = colorResource(id = BankType.NOT_SELECTED.brandColor), + bankType = BankType.NOT_SELECTED, setCardNumber = {}, setExpiredDatedNumber = {}, setOwnerNamedNumber = {}, diff --git a/app/src/main/java/nextstep/payments/ui/card/registration/NewCardViewModel.kt b/app/src/main/java/nextstep/payments/ui/card/registration/NewCardViewModel.kt new file mode 100644 index 00000000..b5774bdc --- /dev/null +++ b/app/src/main/java/nextstep/payments/ui/card/registration/NewCardViewModel.kt @@ -0,0 +1,191 @@ +package nextstep.payments.ui.card.registration + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.createSavedStateHandle +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import nextstep.payments.data.BankType +import nextstep.payments.data.Card +import nextstep.payments.data.PaymentCardsRepository + +class NewCardViewModel( + private val repository: PaymentCardsRepository = PaymentCardsRepository, + private val savedStateHandle: SavedStateHandle +) : ViewModel() { + + private val _cardNumber = MutableStateFlow("") + val cardNumber: StateFlow = _cardNumber.asStateFlow() + + private val _expiredDate = MutableStateFlow("") + val expiredDate: StateFlow = _expiredDate.asStateFlow() + + private val _ownerName = MutableStateFlow("") + val ownerName: StateFlow = _ownerName.asStateFlow() + + private val _password = MutableStateFlow("") + val password: StateFlow = _password.asStateFlow() + + private val _cardAdded = MutableStateFlow(false) + val cardAdded: StateFlow = _cardAdded.asStateFlow() + + private val _selectedBankType = MutableStateFlow(BankType.NOT_SELECTED) + val selectedBankType: StateFlow = _selectedBankType.asStateFlow() + + private var oldCard: Card? = null + + private val _uiState = MutableStateFlow(RegistrationUiState.NewCard) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + openCardData() + } + + fun openCardData() { + savedStateHandle.get(KEY_CARD_NUMBER)?.let { + _cardNumber.value = it + } + savedStateHandle.get(KEY_EXPIRED)?.let { + _expiredDate.value = it + } + + savedStateHandle.get(KEY_OWNER_NAME)?.let { + _ownerName.value = it + } + + savedStateHandle.get(KEY_PASSWORD)?.let { + _password.value = it + } + + savedStateHandle.get(KEY_BANK_TYPE)?.let { + _selectedBankType.value = it + } + + savedStateHandle.get(KEY_UI_STATE)?.let { + _uiState.value = it + } + savedStateHandle.get(KEY_OLD_CARD)?.let { + oldCard = it + } + } + + fun saveCardData() { + savedStateHandle[KEY_CARD_NUMBER] = cardNumber.value + savedStateHandle[KEY_EXPIRED] = expiredDate.value + savedStateHandle[KEY_OWNER_NAME] = ownerName.value + savedStateHandle[KEY_PASSWORD] = password.value + savedStateHandle[KEY_BANK_TYPE] = selectedBankType.value + savedStateHandle[KEY_UI_STATE] = uiState.value + savedStateHandle[KEY_OLD_CARD] = oldCard + } + + fun setCardNumber(cardNumber: String) { + _cardNumber.value = cardNumber + } + + fun setExpiredDate(expiredDate: String) { + _expiredDate.value = expiredDate + } + + fun setOwnerName(ownerName: String) { + _ownerName.value = ownerName + } + + fun setPassword(password: String) { + _password.value = password + } + + fun setSelectBankType(bankType: BankType) { + _selectedBankType.value = bankType + } + + fun saveCard() { + when (uiState.value) { + RegistrationUiState.NewCard -> addCard() + RegistrationUiState.EditCard -> editCard() + } + } + + private fun addCard() { + repository.addCard( + Card( + cardNumber = cardNumber.value, + expiredDate = expiredDate.value, + ownerName = ownerName.value, + password = password.value, + bankType = selectedBankType.value + ) + ) + _cardAdded.value = true + } + + private fun editCard() { + oldCard?.let { + if (isCardDataChange()) { + repository.editCard( + newCard = Card( + id = it.id, + cardNumber = cardNumber.value, + expiredDate = expiredDate.value, + ownerName = ownerName.value, + password = password.value, + bankType = selectedBankType.value + ) + ) + _cardAdded.value = true + } + } + } + + private fun isCardDataChange(): Boolean { + oldCard?.let { + return it.cardNumber != cardNumber.value || + it.expiredDate != expiredDate.value || + it.ownerName != ownerName.value || + it.password != password.value || + it.bankType != selectedBankType.value + } ?: return false + } + + fun setOldCard(card: Card) { + if (savedStateHandle.get(KEY_OLD_CARD) == null) { + _cardNumber.value = card.cardNumber + _expiredDate.value = card.expiredDate + _ownerName.value = card.ownerName + _password.value = card.password + _selectedBankType.value = card.bankType + } + oldCard = card + } + + fun setUiState(newUiState: RegistrationUiState) { + if(savedStateHandle.get(KEY_UI_STATE) == null) { + _uiState.value = newUiState + } + } + + companion object { + private const val TAG = "NewCardViewModel" + private const val KEY_CARD_NUMBER = "cardNumber" + private const val KEY_EXPIRED = "expired" + private const val KEY_OWNER_NAME = "ownerName" + private const val KEY_PASSWORD = "password" + private const val KEY_BANK_TYPE = "bankType" + private const val KEY_UI_STATE = "uiState" + private const val KEY_OLD_CARD = "oldCard" + + val Factory: ViewModelProvider.Factory = viewModelFactory { + initializer { + val savedStateHandle = createSavedStateHandle() + NewCardViewModel( + repository = PaymentCardsRepository, + savedStateHandle = savedStateHandle + ) + } + } + } +} diff --git a/app/src/main/java/nextstep/payments/ui/card/registration/RegistrationUiState.kt b/app/src/main/java/nextstep/payments/ui/card/registration/RegistrationUiState.kt new file mode 100644 index 00000000..a1f213a9 --- /dev/null +++ b/app/src/main/java/nextstep/payments/ui/card/registration/RegistrationUiState.kt @@ -0,0 +1,12 @@ +package nextstep.payments.ui.card.registration + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +sealed interface RegistrationUiState : Parcelable { + @Parcelize + data object NewCard : RegistrationUiState + + @Parcelize + data object EditCard : RegistrationUiState +} diff --git a/app/src/main/java/nextstep/payments/ui/card/registration/component/BankSelectRow.kt b/app/src/main/java/nextstep/payments/ui/card/registration/component/BankSelectRow.kt index 8f195c4a..fe72617b 100644 --- a/app/src/main/java/nextstep/payments/ui/card/registration/component/BankSelectRow.kt +++ b/app/src/main/java/nextstep/payments/ui/card/registration/component/BankSelectRow.kt @@ -1,5 +1,6 @@ package nextstep.payments.ui.card.registration.component +import android.util.Log import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -26,7 +27,7 @@ private const val COLUMN_COUNT = 4 @OptIn(ExperimentalLayoutApi::class) @Composable fun BankSelectRow( - onClick: (BankType) -> Unit = {}, + onClickBankType: (BankType) -> Unit = {}, ) { FlowRow( modifier = Modifier @@ -40,7 +41,7 @@ fun BankSelectRow( if (bankType == BankType.NOT_SELECTED) return@forEach CardSelector( bankType = bankType, - onClick = onClick + onClick = { onClickBankType(bankType) } ) } } @@ -54,7 +55,10 @@ fun CardSelector( Column( modifier = Modifier .size(80.dp) - .clickable { onClick(bankType) }, + .clickable { + Log.e("CardSelector", "CardSelector: $bankType") + onClick(bankType) + }, ) { Image( painter = painterResource(id = bankType.logo),