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
+