diff --git a/README.md b/README.md index bf924a6a..1a8f7cf2 100644 --- a/README.md +++ b/README.md @@ -1 +1,10 @@ -# android-payments \ No newline at end of file +# Step 2 - 페이먼츠(카드 목록) + +## 기능 목록 +- [O] 카드 목록 화면 구현 +- [O] 카드 목록 화면 -> 카드 추가 화면 연결 +- [] 새로운 카드가 추가되었을 때 카드 목록이 업데이트 되어야 한다. +- [O] 카드 목록이 비어있다면 카드 등록 코멘트 Text 노출 +- [] 카드 목록에 카드가 한 개 있을 경우 카드 추가 UI는 목록 하단에 노출한다 +- [] 카드 목록에 카드가 두 개 이상 있을 경우 카드 추가 UI는 상단바에 노출된다. +- [] 카드 목록 데이터를 보관하기 위해 ViewModel과 StateFlow를 활용한다 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 new file mode 100644 index 00000000..c0f91153 --- /dev/null +++ b/app/src/androidTest/java/nextstep/payments/screen/card/list/RegisteredCreditCardsScreenTest.kt @@ -0,0 +1,150 @@ +package nextstep.payments.screen.card.list + +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onAllNodesWithTag +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import nextstep.payments.data.BcCard +import nextstep.payments.data.Card +import nextstep.payments.data.PaymentCardsRepository +import nextstep.payments.data.RegisteredCreditCards +import nextstep.payments.ui.card.list.CardListScreen +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class RegisteredCreditCardsScreenTest { + + @get:Rule + val composeRule = createComposeRule() + + @Before + fun setUp() { + PaymentCardsRepository.removeAllCard() + } + + @Test + fun 스크린_상단에_Payments_타이틀이_노출된다() { + // when : cardListScreen을 setContent 한다. + composeRule + .setContent { + CardListScreen( + registeredCreditCards = RegisteredCreditCards(mutableListOf()), + onAddCard = {} + ) + } + + // then : Payments 타이틀에 노출되어야한다. + composeRule + .onNodeWithText("Payments") + .assertExists() + } + + @Test + fun 등록된_카드가_존재할_경우_해당_카드_수만큼_카드_이미지를_노출시키다() { + // given : 두 개의 카드를 등록한다 + val registeredCreditCards = RegisteredCreditCards( + mutableListOf( + Card( + cardNumber = "1234-5678-1234-5678", + ownerName = "홍길동", + expiredDate = "12/24", + password = "123", + cardCompany = BcCard + ), + Card( + cardNumber = "1234-5678-1234-5628", + ownerName = "홍길동", + expiredDate = "12/24", + password = "123", + cardCompany = BcCard + ) + ) + ) + + // when : 화면을 렌더링한다. + composeRule.setContent { + CardListScreen( + registeredCreditCards = registeredCreditCards + ) + } + val actual = composeRule + .onAllNodesWithTag("cardImage") + .fetchSemanticsNodes().size + + // then : 카드 이미지 두개가 노출되어야 한다. + assert(actual == 2) + } + + @Test + fun 등록된_카드가_존재하지_않을_경우_해당_카드_이미지를_노출하지_않는다() { + // when : 화면을 렌더링한다. + composeRule.setContent { + CardListScreen( + registeredCreditCards = RegisteredCreditCards(mutableListOf()) + ) + } + + // then : 카드 이미지가 노출되지 않는다. + composeRule + .onNodeWithTag("cardImage") + .assertDoesNotExist() + } + + @Test + fun 등록된_카드가_존재하지_않을_경우_카드_추가_멘트가_노출된다() { + // given : 카드 등록이 되어있지 않다. + + + // when : 화면을 렌더링한다. +// cardImage + composeRule.setContent { + CardListScreen( + registeredCreditCards = RegisteredCreditCards(mutableListOf()) + ) + } + + // then : 카드 추가 멘트가 노출된다. + composeRule + .onNodeWithTag("textComment") + .assertExists() + } + + @Test + fun 등록된_카드가_존재할_경우_카드_추가_멘트가_노출되자_않는다() { + // given : 카드 등록이 되어있다. + PaymentCardsRepository.addCard( + Card( + cardNumber = "1234-5678-1234-5628", + ownerName = "홍길동", + expiredDate = "12/24", + password = "123", + cardCompany = BcCard + ) + ) + val registeredCreditCards = RegisteredCreditCards( + mutableListOf( + Card( + cardNumber = "1234-5678-1234-5628", + ownerName = "홍길동", + expiredDate = "12/24", + password = "123", + cardCompany = BcCard + ) + ) + ) + + + // when : 화면을 렌더링한다. + composeRule.setContent { + CardListScreen( + registeredCreditCards = registeredCreditCards + ) + } + + // then : 카드 추가 멘트가 노출되지 않는다 + composeRule + .onNodeWithTag("textComment") + .assertDoesNotExist() + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 40ffa6e0..51173918 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -23,6 +23,7 @@ + diff --git a/app/src/main/java/nextstep/payments/MainActivity.kt b/app/src/main/java/nextstep/payments/MainActivity.kt index b0fd9b7a..6656f923 100644 --- a/app/src/main/java/nextstep/payments/MainActivity.kt +++ b/app/src/main/java/nextstep/payments/MainActivity.kt @@ -1,29 +1,47 @@ package nextstep.payments +import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity +import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.setContent +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import nextstep.payments.ui.NewCardScreen +import nextstep.payments.ui.card.list.CardListScreen +import nextstep.payments.ui.card.list.CardListViewModel +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() + setContent { + val launcher = + rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == RESULT_OK) { + viewModel.fetchCards() + } + } + PaymentsTheme { - // A surface container using the 'background' color from the theme Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { - NewCardScreen() + CardListScreen( + viewModel = viewModel, + onAddCard = { + val intent = Intent(this, NewCardActivity::class.java) + launcher.launch(intent) + } + ) } } } diff --git a/app/src/main/java/nextstep/payments/NewCardViewModel.kt b/app/src/main/java/nextstep/payments/NewCardViewModel.kt index 8d1dd190..18e36130 100644 --- a/app/src/main/java/nextstep/payments/NewCardViewModel.kt +++ b/app/src/main/java/nextstep/payments/NewCardViewModel.kt @@ -4,8 +4,12 @@ import androidx.lifecycle.ViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import nextstep.payments.data.Card +import nextstep.payments.data.PaymentCardsRepository -class NewCardViewModel : ViewModel() { +class NewCardViewModel( + private val repository: PaymentCardsRepository = PaymentCardsRepository +) : ViewModel() { private val _cardNumber = MutableStateFlow("") val cardNumber: StateFlow = _cardNumber.asStateFlow() @@ -19,6 +23,10 @@ class NewCardViewModel : ViewModel() { 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 } @@ -34,4 +42,9 @@ class NewCardViewModel : ViewModel() { fun setPassword(password: String) { _password.value = password } -} \ No newline at end of file + + fun addCard(card: Card) { + repository.addCard(card) + _cardAdded.value = true + } +} diff --git a/app/src/main/java/nextstep/payments/data/Card.kt b/app/src/main/java/nextstep/payments/data/Card.kt new file mode 100644 index 00000000..aa86de12 --- /dev/null +++ b/app/src/main/java/nextstep/payments/data/Card.kt @@ -0,0 +1,9 @@ +package nextstep.payments.data + +data class Card( + val cardNumber: String, + val expiredDate: String, + val ownerName: String, + val password: String, + val cardCompany: CardCompany = BcCard +) diff --git a/app/src/main/java/nextstep/payments/data/CardCompany.kt b/app/src/main/java/nextstep/payments/data/CardCompany.kt new file mode 100644 index 00000000..7c085294 --- /dev/null +++ b/app/src/main/java/nextstep/payments/data/CardCompany.kt @@ -0,0 +1,5 @@ +package nextstep.payments.data + +sealed class CardCompany + +data object BcCard : CardCompany() diff --git a/app/src/main/java/nextstep/payments/data/PaymentCardsRepository.kt b/app/src/main/java/nextstep/payments/data/PaymentCardsRepository.kt new file mode 100644 index 00000000..a0e8a0e9 --- /dev/null +++ b/app/src/main/java/nextstep/payments/data/PaymentCardsRepository.kt @@ -0,0 +1,15 @@ +package nextstep.payments.data + +object PaymentCardsRepository { + + private val _cards = mutableListOf() + val cards: List get() = _cards.toList() + + fun addCard(card: Card) { + _cards.add(card) + } + + fun removeAllCard() { + _cards.clear() + } +} diff --git a/app/src/main/java/nextstep/payments/data/RegisteredCreditCards.kt b/app/src/main/java/nextstep/payments/data/RegisteredCreditCards.kt new file mode 100644 index 00000000..fb8588ef --- /dev/null +++ b/app/src/main/java/nextstep/payments/data/RegisteredCreditCards.kt @@ -0,0 +1,14 @@ +package nextstep.payments.data + +import nextstep.payments.ui.card.CreditCardUiState + +data class RegisteredCreditCards(val cardList: List) { + + fun getState(): CreditCardUiState { + return when (cardList.size) { + 0 -> CreditCardUiState.Empty + 1 -> CreditCardUiState.One(cardList[0]) + else -> CreditCardUiState.Many(cardList) + } + } +} diff --git a/app/src/main/java/nextstep/payments/ui/PaymentCard.kt b/app/src/main/java/nextstep/payments/ui/PaymentCard.kt index cd3c7290..36cd0748 100644 --- a/app/src/main/java/nextstep/payments/ui/PaymentCard.kt +++ b/app/src/main/java/nextstep/payments/ui/PaymentCard.kt @@ -2,6 +2,9 @@ package nextstep.payments.ui import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape @@ -12,6 +15,11 @@ import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import nextstep.payments.data.BcCard +import nextstep.payments.data.Card +import nextstep.payments.ui.card.list.component.card.CardExpiredDate +import nextstep.payments.ui.card.list.component.card.CardNumber +import nextstep.payments.ui.card.list.component.card.CardOwnerName @Composable fun PaymentCard( @@ -39,8 +47,77 @@ fun PaymentCard( } } +@Composable +fun PaymentCard( + card: Card, + cardColor: Color, + modifier: Modifier = Modifier, +) { + Box( + contentAlignment = Alignment.BottomEnd, + modifier = modifier + .shadow(8.dp) + .size(width = 208.dp, height = 124.dp) + .background( + color = cardColor, + shape = RoundedCornerShape(5.dp), + ) + .padding(bottom = 10.dp) + ) { + Column( + ) { + Box( + modifier = Modifier + .padding(start = 14.dp, bottom = 10.dp) + .size(width = 40.dp, height = 26.dp) + .background( + color = Color(0xFFCBBA64), + shape = RoundedCornerShape(4.dp), + ) + ) + + CardNumber( + cardNumber = card.cardNumber, + modifier = Modifier.padding(start = 14.dp, end = 14.dp) + ) + + Row { + CardOwnerName( + ownerName = card.ownerName, + modifier = Modifier.padding(start = 14.dp) + ) + + Spacer(modifier = Modifier.weight(1f)) + + CardExpiredDate( + expiredDate = card.expiredDate, + modifier = Modifier.padding(end = 14.dp) + ) + } + } + + } +} + @Preview @Composable private fun PaymentCardPreview() { PaymentCard() -} \ No newline at end of file +} + +@Preview +@Composable +private fun PaymentCardInfoPreview() { + PaymentCard( + Card( + cardNumber = "1234-5678-1234-5678", + ownerName = "홍길동", + expiredDate = "12/34", + password = "123", + cardCompany = BcCard + ), + cardColor = Color(0xff333333), + modifier = Modifier + .size(width = 208.dp, height = 124.dp) + ) +} diff --git a/app/src/main/java/nextstep/payments/ui/card/CreditCardUiState.kt b/app/src/main/java/nextstep/payments/ui/card/CreditCardUiState.kt new file mode 100644 index 00000000..0014dc80 --- /dev/null +++ b/app/src/main/java/nextstep/payments/ui/card/CreditCardUiState.kt @@ -0,0 +1,9 @@ +package nextstep.payments.ui.card + +import nextstep.payments.data.Card + +sealed interface CreditCardUiState { + data object Empty : CreditCardUiState + data class One(val card: Card) : CreditCardUiState + data class Many(val cards: List) : CreditCardUiState +} 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 new file mode 100644 index 00000000..f106cf96 --- /dev/null +++ b/app/src/main/java/nextstep/payments/ui/card/list/CardListScreen.kt @@ -0,0 +1,206 @@ +package nextstep.payments.ui.card.list + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import nextstep.payments.R +import nextstep.payments.data.BcCard +import nextstep.payments.data.Card +import nextstep.payments.data.PaymentCardsRepository +import nextstep.payments.data.RegisteredCreditCards +import nextstep.payments.ui.card.CreditCardUiState +import nextstep.payments.ui.card.list.component.card.CardLazyColumn +import nextstep.payments.ui.card.list.component.card.CardListTopBar +import nextstep.payments.ui.card.list.component.card.CardListTopBarWithAdd +import nextstep.payments.ui.card.list.component.card.EmptyCardImage +import nextstep.payments.ui.theme.PaymentsTheme + +@Composable +fun CardListScreen( + viewModel: CardListViewModel = viewModel(), + onAddCard: () -> Unit = {}, +) { + val cards by viewModel.registeredCreditCards.collectAsStateWithLifecycle() + + CardListScreen( + registeredCreditCards = cards, + onAddCard = onAddCard, + ) +} + +@Composable +fun CardListScreen( + registeredCreditCards: RegisteredCreditCards = RegisteredCreditCards(mutableListOf()), + onAddCard: () -> Unit = {} +) { + + when (registeredCreditCards.getState()) { + is CreditCardUiState.Empty -> { + CardListScreenEmpty( + comment = stringResource(id = R.string.text_card_registration_comment), + onAddCard = onAddCard, + ) + } + + is CreditCardUiState.One -> { + CardListScreenOne( + cards = registeredCreditCards.cardList, + onAddCard = onAddCard, + ) + } + + is CreditCardUiState.Many -> { + CardListScreenMany( + cards = registeredCreditCards.cardList, + onAddCard = onAddCard, + ) + } + } +} + +@Composable +fun CardListScreenEmpty( + comment: String, + onAddCard: () -> Unit = {} +) { + Scaffold(topBar = { CardListTopBar() }) { paddingValues -> + Column( + modifier = Modifier.padding(paddingValues) + ) { + Text( + text = comment, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 73.dp) + ) + + EmptyCardImage( + cardColor = Color(0xFFE5E5E5), + onAddCard = onAddCard, + modifier = Modifier + .fillMaxWidth() + .padding( + start = 73.dp, end = 73.dp, top = 12.dp, bottom = 24.dp + ) + .size(width = 208.dp, height = 124.dp) + ) + } + } +} + +@Composable +fun CardListScreenOne( + cards: List, + onAddCard: () -> Unit = {} +) { + Scaffold(topBar = { CardListTopBar() }) { paddingValues -> + Column( + modifier = Modifier.padding(paddingValues) + ) { + CardLazyColumn(cards) + + EmptyCardImage( + cardColor = Color(0xFFE5E5E5), + onAddCard = onAddCard, + modifier = Modifier + .fillMaxWidth() + .padding( + start = 73.dp, end = 73.dp, top = 12.dp, bottom = 24.dp + ) + .size(width = 208.dp, height = 124.dp) + ) + } + } +} + +@Composable +fun CardListScreenMany( + cards: List, + onAddCard: () -> Unit = {} +) { + Scaffold(topBar = { + CardListTopBarWithAdd( + onClickAdd = onAddCard + ) + }) { paddingValues -> + Column( + modifier = Modifier.padding(paddingValues) + ) { + CardLazyColumn(cards) + } + } +} + +@Preview +@Composable +private fun CardListScreenEmptyPreview() { + PaymentCardsRepository.removeAllCard() + PaymentsTheme { + CardListScreen( + registeredCreditCards = RegisteredCreditCards(mutableListOf()), + onAddCard = {} + ) + } +} + +@Preview +@Composable +private fun CardListScreenOnePreview() { + PaymentsTheme { + CardListScreen( + registeredCreditCards = RegisteredCreditCards( + cardList = listOf( + Card( + cardNumber = "1234-5678-1234-6654", + ownerName = "홍길동", + expiredDate = "12/24", + password = "123", + cardCompany = BcCard + ) + ) + ), + onAddCard = {} + ) + } +} + +@Preview +@Composable +private fun CardListScreenManyPreview() { + val registeredCreditCards = RegisteredCreditCards( + cardList = listOf( + Card( + cardNumber = "1234-5678-1234-6654", + ownerName = "홍길동", + expiredDate = "12/24", + password = "123", + cardCompany = BcCard + ), + Card( + cardNumber = "1234-5678-1234-1234", + ownerName = "홍길동", + expiredDate = "12/24", + password = "123", + cardCompany = BcCard + ) + ) + ) + + PaymentsTheme { + CardListScreen(registeredCreditCards = registeredCreditCards, onAddCard = {}) + } +} + + 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 new file mode 100644 index 00000000..7ff8404d --- /dev/null +++ b/app/src/main/java/nextstep/payments/ui/card/list/CardListViewModel.kt @@ -0,0 +1,18 @@ +package nextstep.payments.ui.card.list + +import androidx.lifecycle.ViewModel +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 + +class CardListViewModel : ViewModel() { + + private val _registeredCreditCards = MutableStateFlow(RegisteredCreditCards(emptyList())) + val registeredCreditCards: StateFlow = _registeredCreditCards.asStateFlow() + + fun fetchCards() { + _registeredCreditCards.value = RegisteredCreditCards(PaymentCardsRepository.cards) + } +} diff --git a/app/src/main/java/nextstep/payments/ui/card/list/component/card/CardExpiredDate.kt b/app/src/main/java/nextstep/payments/ui/card/list/component/card/CardExpiredDate.kt new file mode 100644 index 00000000..d1cde39a --- /dev/null +++ b/app/src/main/java/nextstep/payments/ui/card/list/component/card/CardExpiredDate.kt @@ -0,0 +1,27 @@ +package nextstep.payments.ui.card.list.component.card + +import androidx.compose.material3.MaterialTheme +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 nextstep.payments.ui.theme.PaymentsTheme + +@Composable +fun CardExpiredDate(expiredDate: String, modifier: Modifier = Modifier) { + Text( + text = expiredDate, + modifier = modifier, + style = MaterialTheme.typography.bodyMedium, + color = Color.White + ) +} + +@Preview +@Composable +private fun CardExpiredDatePreview() { + PaymentsTheme { + CardExpiredDate("12/24") + } +} 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 new file mode 100644 index 00000000..591b9d00 --- /dev/null +++ b/app/src/main/java/nextstep/payments/ui/card/list/component/card/CardLazyColumn.kt @@ -0,0 +1,33 @@ +package nextstep.payments.ui.card.list.component.card + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import nextstep.payments.data.Card +import nextstep.payments.data.RegisteredCreditCards +import nextstep.payments.data.PaymentCardsRepository +import nextstep.payments.ui.PaymentCard + +@Composable +fun CardLazyColumn(cards: List) { + LazyColumn { + items( + count = cards.size, + key = { index -> cards[index].cardNumber } + ) { + PaymentCard( + card = cards[it], + cardColor = Color(0xff333333), + modifier = Modifier.padding( + start = 73.dp, + end = 73.dp, + top = 12.dp, + bottom = 24.dp + ) + ) + } + } +} diff --git a/app/src/main/java/nextstep/payments/ui/card/list/component/card/CardListTopBar.kt b/app/src/main/java/nextstep/payments/ui/card/list/component/card/CardListTopBar.kt new file mode 100644 index 00000000..a8827f68 --- /dev/null +++ b/app/src/main/java/nextstep/payments/ui/card/list/component/card/CardListTopBar.kt @@ -0,0 +1,27 @@ +package nextstep.payments.ui.card.list.component.card + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CardListTopBar(modifier: Modifier = Modifier) { + TopAppBar( + title = { + Text( + text = "Payments", + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleLarge + ) + }, + modifier = Modifier + ) +} diff --git a/app/src/main/java/nextstep/payments/ui/card/list/component/card/CardListTopBarWithAdd.kt b/app/src/main/java/nextstep/payments/ui/card/list/component/card/CardListTopBarWithAdd.kt new file mode 100644 index 00000000..135be4dd --- /dev/null +++ b/app/src/main/java/nextstep/payments/ui/card/list/component/card/CardListTopBarWithAdd.kt @@ -0,0 +1,47 @@ +package nextstep.payments.ui.card.list.component.card + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import nextstep.payments.ui.theme.PaymentsTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CardListTopBarWithAdd(modifier: Modifier = Modifier, onClickAdd: () -> Unit = {}) { + TopAppBar( + title = { + Text( + text = "Payments", + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleLarge + ) + }, + actions = { + IconButton( + onClick = onClickAdd + ) { + Text( + text = "추가" + ) + } + }, + modifier = Modifier + ) +} + +@Preview +@Composable +private fun CardListManyTopBarPreview() { + PaymentsTheme { + CardListTopBarWithAdd() + } +} + diff --git a/app/src/main/java/nextstep/payments/ui/card/list/component/card/CardNumber.kt b/app/src/main/java/nextstep/payments/ui/card/list/component/card/CardNumber.kt new file mode 100644 index 00000000..4e1ee520 --- /dev/null +++ b/app/src/main/java/nextstep/payments/ui/card/list/component/card/CardNumber.kt @@ -0,0 +1,75 @@ +package nextstep.payments.ui.card.list.component.card + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.MaterialTheme +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 nextstep.payments.ui.theme.PaymentsTheme + +@Composable +fun CardNumber(cardNumber: String, modifier: Modifier = Modifier) { + val numberList = cardNumber.split("-") + + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = numberList[0], + color = Color.White, + style = MaterialTheme.typography.bodyMedium, + ) + + Text( + text = "-", + color = Color.White, + style = MaterialTheme.typography.bodyMedium, + ) + + Text( + text = numberList[1], + color = Color.White, + style = MaterialTheme.typography.bodyMedium, + ) + + Text( + text = "-", + color = Color.White, + style = MaterialTheme.typography.bodyMedium, + ) + + Text( + text = numberList[2], + color = Color.White, + style = MaterialTheme.typography.bodyMedium, + ) + + Text( + text = "-", + color = Color.White, + style = MaterialTheme.typography.bodyMedium, + ) + + Text( + text = numberList[3], + color = Color.White, + style = MaterialTheme.typography.bodyMedium, + ) + } +} + +@Preview( + showBackground = true, + backgroundColor = 0xFF333333 +) +@Composable +private fun CardNumberPreview() { + PaymentsTheme { + CardNumber(cardNumber = "9999-1234-5678-0000") + } +} diff --git a/app/src/main/java/nextstep/payments/ui/card/list/component/card/CardOwnerName.kt b/app/src/main/java/nextstep/payments/ui/card/list/component/card/CardOwnerName.kt new file mode 100644 index 00000000..f4cf85b9 --- /dev/null +++ b/app/src/main/java/nextstep/payments/ui/card/list/component/card/CardOwnerName.kt @@ -0,0 +1,17 @@ +package nextstep.payments.ui.card.list.component.card + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color + +@Composable +fun CardOwnerName(ownerName: String, modifier: Modifier = Modifier) { + Text( + text = ownerName, + modifier = modifier, + style = MaterialTheme.typography.bodyMedium, + color = Color.White + ) +} 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 new file mode 100644 index 00000000..cff42059 --- /dev/null +++ b/app/src/main/java/nextstep/payments/ui/card/list/component/card/EmptyCardImage.kt @@ -0,0 +1,55 @@ +package nextstep.payments.ui.card.list.component.card + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +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 +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import nextstep.payments.ui.theme.PaymentsTheme + +@Composable +fun EmptyCardImage(cardColor: Color, onAddCard: () -> Unit, modifier: Modifier = Modifier) { + Card( + modifier = modifier + .clip(shape = CardDefaults.shape) + .clickable { onAddCard() }, + colors = CardDefaults.cardColors( + containerColor = cardColor, + ) + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Filled.Add, + contentDescription = null, + ) + } + } +} + +@Preview +@Composable +private fun EmptyCardImagePreview() { + PaymentsTheme { + EmptyCardImage( + cardColor = Color(0xFFE5E5E5), + onAddCard = {}, + modifier = Modifier + .size(width = 208.dp, height = 124.dp) + ) + } +} diff --git a/app/src/main/java/nextstep/payments/ui/card/list/component/card/IntegratedCircuit.kt b/app/src/main/java/nextstep/payments/ui/card/list/component/card/IntegratedCircuit.kt new file mode 100644 index 00000000..3af4fdc1 --- /dev/null +++ b/app/src/main/java/nextstep/payments/ui/card/list/component/card/IntegratedCircuit.kt @@ -0,0 +1,37 @@ +package nextstep.payments.ui.card.list.component.card + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +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 nextstep.payments.ui.theme.PaymentsTheme + +@Composable +fun IntegratedCircuit(modifier: Modifier) { + Spacer( + modifier = modifier + .background( + Color(0xFFCBBA64), + shape = MaterialTheme.shapes.small + ) + ) +} + + +@Preview +@Composable +private fun IntegratedCircuitPreview() { + PaymentsTheme { + IntegratedCircuit( + modifier = Modifier + .padding(top = 44.dp, start = 14.dp) + .size(width = 40.dp, height = 26.dp) + ) + } +} 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 new file mode 100644 index 00000000..00013407 --- /dev/null +++ b/app/src/main/java/nextstep/payments/ui/card/registration/NewCardActivity.kt @@ -0,0 +1,33 @@ +package nextstep.payments.ui.card.registration + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import nextstep.payments.NewCardViewModel +import nextstep.payments.ui.theme.PaymentsTheme + +class NewCardActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + + val viewModel by viewModels() + + super.onCreate(savedInstanceState) + setContent { + PaymentsTheme { + NewCardScreen( + viewModel = viewModel, + navigateToCardList = { + setResult(RESULT_OK) + finish() + }, + onBackClick = { + finish() + } + ) + } + } + } +} + + diff --git a/app/src/main/java/nextstep/payments/ui/NewCardScreen.kt b/app/src/main/java/nextstep/payments/ui/card/registration/NewCardScreen.kt similarity index 65% rename from app/src/main/java/nextstep/payments/ui/NewCardScreen.kt rename to app/src/main/java/nextstep/payments/ui/card/registration/NewCardScreen.kt index e9e3b4bf..72d7f2ef 100644 --- a/app/src/main/java/nextstep/payments/ui/NewCardScreen.kt +++ b/app/src/main/java/nextstep/payments/ui/card/registration/NewCardScreen.kt @@ -1,4 +1,4 @@ -package nextstep.payments.ui +package nextstep.payments.ui.card.registration import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -10,21 +10,30 @@ import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.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.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import nextstep.payments.NewCardViewModel +import nextstep.payments.R +import nextstep.payments.data.BcCard +import nextstep.payments.data.Card +import nextstep.payments.ui.PaymentCard +import nextstep.payments.ui.card.registration.component.NewCardTopBar import nextstep.payments.ui.theme.PaymentsTheme // Stateful @Composable fun NewCardScreen( modifier: Modifier = Modifier, + navigateToCardList: () -> Unit, + onBackClick: () -> Unit = {}, viewModel: NewCardViewModel = viewModel(), ) { val cardNumber by viewModel.cardNumber.collectAsStateWithLifecycle() @@ -32,7 +41,14 @@ fun NewCardScreen( val ownerName by viewModel.ownerName.collectAsStateWithLifecycle() val password by viewModel.password.collectAsStateWithLifecycle() + val cardAdded by viewModel.cardAdded.collectAsStateWithLifecycle() + + LaunchedEffect(cardAdded) { + if (cardAdded) navigateToCardList() + } + NewCardScreen( + modifier = modifier, cardNumber = cardNumber, expiredDate = expiredDate, ownerName = ownerName, @@ -41,13 +57,15 @@ fun NewCardScreen( setExpiredDatedNumber = viewModel::setExpiredDate, setOwnerNamedNumber = viewModel::setOwnerName, setPasswordNumber = viewModel::setPassword, - modifier = modifier + onBackClick = onBackClick, + onSaveClick = viewModel::addCard ) } // Stateless @Composable private fun NewCardScreen( + modifier: Modifier = Modifier, cardNumber: String, expiredDate: String, ownerName: String, @@ -56,11 +74,23 @@ private fun NewCardScreen( setExpiredDatedNumber: (String) -> Unit, setOwnerNamedNumber: (String) -> Unit, setPasswordNumber: (String) -> Unit, - modifier: Modifier = Modifier + onBackClick: () -> Unit = {}, + onSaveClick: (Card) -> Unit = {} ) { Scaffold( - topBar = { NewCardTopBar(onBackClick = { TODO() }, onSaveClick = { TODO() }) }, - modifier = modifier + topBar = { + NewCardTopBar(onBackClick = onBackClick, onSaveClick = { + onSaveClick( + Card( + cardNumber = cardNumber, + expiredDate = expiredDate, + ownerName = ownerName, + password = password, + cardCompany = BcCard + ) + ) + }) + }, modifier = modifier ) { innerPadding -> Column( verticalArrangement = Arrangement.spacedBy(18.dp), @@ -78,32 +108,32 @@ private fun NewCardScreen( OutlinedTextField( value = cardNumber, onValueChange = { setCardNumber(it) }, - label = { Text("카드 번호") }, - placeholder = { Text("0000 - 0000 - 0000 - 0000") }, + label = { Text(stringResource(id = R.string.text_card_number)) }, + placeholder = { Text(stringResource(id = R.string.placeholder_card_number)) }, modifier = Modifier.fillMaxWidth(), ) OutlinedTextField( value = expiredDate, onValueChange = { setExpiredDatedNumber(it) }, - label = { Text("만료일") }, - placeholder = { Text("MM / YY") }, + label = { Text(stringResource(id = R.string.text_card_expiration_date)) }, + placeholder = { Text(stringResource(id = R.string.placeholder_card_expiration_date)) }, modifier = Modifier.fillMaxWidth(), ) OutlinedTextField( value = ownerName, onValueChange = { setOwnerNamedNumber(it) }, - label = { Text("카드 소유자 이름(선택)") }, - placeholder = { Text("카드에 표시된 이름을 입력하세요.") }, + label = { Text(stringResource(id = R.string.text_card_owner_name)) }, + placeholder = { Text(stringResource(id = R.string.placeholder_card_owner_name)) }, modifier = Modifier.fillMaxWidth(), ) OutlinedTextField( value = password, onValueChange = { setPasswordNumber(it) }, - label = { Text("비밀번호") }, - placeholder = { Text("0000") }, + label = { Text(stringResource(id = R.string.text_password)) }, + placeholder = { Text(stringResource(id = R.string.placeholder_password)) }, modifier = Modifier.fillMaxWidth(), visualTransformation = PasswordVisualTransformation(), ) @@ -115,14 +145,14 @@ private fun NewCardScreen( @Composable private fun NewCardScreenPreview() { PaymentsTheme { - NewCardScreen( + NewCardScreen(modifier = Modifier, + navigateToCardList = {}, viewModel = NewCardViewModel().apply { setCardNumber("0000 - 0000 - 0000 -0000") setExpiredDate("02/26") setOwnerName("김수현") setPassword("1234") - } - ) + }) } } @@ -130,15 +160,13 @@ private fun NewCardScreenPreview() { @Composable private fun StatelessNewCardScreenPreview() { PaymentsTheme { - NewCardScreen( - cardNumber = "0000 - 0000 - 0000 -0000", + NewCardScreen(cardNumber = "0000 - 0000 - 0000 -0000", expiredDate = "02/26", ownerName = "김수현", password = "1234", setCardNumber = {}, setExpiredDatedNumber = {}, setOwnerNamedNumber = {}, - setPasswordNumber = {} - ) + setPasswordNumber = {}) } -} \ No newline at end of file +} diff --git a/app/src/main/java/nextstep/payments/ui/NewCardTopBar.kt b/app/src/main/java/nextstep/payments/ui/card/registration/component/NewCardTopBar.kt similarity index 83% rename from app/src/main/java/nextstep/payments/ui/NewCardTopBar.kt rename to app/src/main/java/nextstep/payments/ui/card/registration/component/NewCardTopBar.kt index 874f0607..760aa7ad 100644 --- a/app/src/main/java/nextstep/payments/ui/NewCardTopBar.kt +++ b/app/src/main/java/nextstep/payments/ui/card/registration/component/NewCardTopBar.kt @@ -1,4 +1,4 @@ -package nextstep.payments.ui +package nextstep.payments.ui.card.registration.component import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack @@ -10,6 +10,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -38,4 +39,13 @@ fun NewCardTopBar( }, modifier = modifier ) -} \ No newline at end of file +} + +@Preview +@Composable +private fun NewCardTopBarPreview() { + NewCardTopBar( + onBackClick = { }, + onSaveClick = { }, + ) +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9686ba95..a1a3205f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,14 @@ Payments - \ No newline at end of file + 카드 번호 + 0000 - 0000 - 0000 - 0000 + 만료일 + MM / YY + 카드 소유자 이름(선택) + 카드에 표시된 이름을 입력하세요. + 비밀번호 + 0000 + 새로운 카드를 등록해 주세요. + cardImage + textComment +