From 6a39f5e3cb1bb1d7e0ff083c5bee34b90725f67e Mon Sep 17 00:00:00 2001 From: DoTheBestMayB Date: Tue, 21 Oct 2025 20:04:45 +0900 Subject: [PATCH 1/5] =?UTF-8?q?docs:=20Step4=20=EC=9A=94=EA=B5=AC=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20README=EC=97=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 531ad59e..3c84c780 100644 --- a/README.md +++ b/README.md @@ -10,3 +10,9 @@ - 카드 추가 화면 - [x] 접속시 카드사 선택 bottom sheet 보여주기 - [x] 카드 미리 보기에 카드사 정보 표시하기 + +## Step4 + +- [ ] 카드 목록에서 카드를 선택하면 카드 수정 화면으로 이동한다. +- [ ] 카드 수정 화면에서 변경사항이 발생하지 않으면 수정이 불가능하다. +- [ ] 카드가 수정되면 카드 목록 화면에 변경사항이 반영된다. From 00ad3622e29e47f52474717f6af13a149a214967 Mon Sep 17 00:00:00 2001 From: DoTheBestMayB Date: Tue, 21 Oct 2025 21:52:20 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=EC=B9=B4=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=ED=99=94=EB=A9=B4=EC=9C=BC=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- app/build.gradle.kts | 1 + .../java/nextstep/payments/MainActivity.kt | 9 +++- .../data/card/PaymentCardRepository.kt | 11 +++++ .../payments/ui/card_list/CardListScreen.kt | 14 ++++++ .../payments/ui/common/model/CreditCard.kt | 8 +-- .../nextstep/payments/ui/mapper/CardMapper.kt | 10 ++++ .../nextstep/payments/ui/new_card/CardType.kt | 5 +- .../payments/ui/new_card/NewCardScreen.kt | 14 +++++- .../payments/ui/new_card/NewCardState.kt | 10 ++++ .../payments/ui/new_card/NewCardTopBar.kt | 3 +- .../payments/ui/new_card/NewCardViewModel.kt | 49 +++++++++++++++++-- app/src/main/res/values/strings.xml | 2 + build.gradle.kts | 1 + gradle/libs.versions.toml | 1 + 15 files changed, 128 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 3c84c780..0935f695 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,6 @@ ## Step4 -- [ ] 카드 목록에서 카드를 선택하면 카드 수정 화면으로 이동한다. -- [ ] 카드 수정 화면에서 변경사항이 발생하지 않으면 수정이 불가능하다. +- [x] 카드 목록에서 카드를 선택하면 카드 수정 화면으로 이동한다. +- [x] 카드 수정 화면에서 변경사항이 발생하지 않으면 수정이 불가능하다. - [ ] 카드가 수정되면 카드 목록 화면에 변경사항이 반영된다. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4c415be0..c273c1b8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -2,6 +2,7 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.jetbrains.kotlin.android) alias(libs.plugins.compose.compiler) + alias(libs.plugins.parcelize) } android { diff --git a/app/src/main/java/nextstep/payments/MainActivity.kt b/app/src/main/java/nextstep/payments/MainActivity.kt index db61ac46..a2efd838 100644 --- a/app/src/main/java/nextstep/payments/MainActivity.kt +++ b/app/src/main/java/nextstep/payments/MainActivity.kt @@ -10,6 +10,7 @@ import androidx.activity.viewModels import nextstep.payments.ui.card_list.CardListScreenRoot import nextstep.payments.ui.card_list.CardListViewModel import nextstep.payments.ui.new_card.NewCardActivity +import nextstep.payments.ui.new_card.NewCardViewModel import nextstep.payments.ui.theme.PaymentsTheme class MainActivity : ComponentActivity() { @@ -33,7 +34,13 @@ class MainActivity : ComponentActivity() { navigateToNewCard = { val intent = Intent(this, NewCardActivity::class.java) launcher.launch(intent) - } + }, + navigateToEditCard = { + val intent = Intent(this, NewCardActivity::class.java).apply { + putExtra(NewCardViewModel.CARD_INFO_KEY, it) + } + launcher.launch(intent) + }, ) } } diff --git a/app/src/main/java/nextstep/payments/data/card/PaymentCardRepository.kt b/app/src/main/java/nextstep/payments/data/card/PaymentCardRepository.kt index 4b7a1a8f..a56a38a8 100644 --- a/app/src/main/java/nextstep/payments/data/card/PaymentCardRepository.kt +++ b/app/src/main/java/nextstep/payments/data/card/PaymentCardRepository.kt @@ -15,4 +15,15 @@ object PaymentCardRepository { _cards.add(cardEntity) return true } + + fun getPassword(target: CardEntity): String { + for (card in cards) { + // 카드 번호는 고유한 값이므로 카드 번호만 비교하도록 구현했습니다. + if (card.cardNumber == target.cardNumber) { + return card.password + } + } + + return "" + } } 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 27aec90c..2b71c9cf 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 @@ -41,6 +41,7 @@ import nextstep.payments.ui.theme.PaymentsTheme fun CardListScreenRoot( viewModel: CardListViewModel, navigateToNewCard: () -> Unit, + navigateToEditCard: (CreditCard) -> Unit, modifier: Modifier = Modifier, ) { val state = viewModel.state.collectAsStateWithLifecycle() @@ -48,6 +49,7 @@ fun CardListScreenRoot( CardListScreen( state = state.value, navigateToNewCard = navigateToNewCard, + navigateToEditCard = navigateToEditCard, modifier = modifier, ) } @@ -56,6 +58,7 @@ fun CardListScreenRoot( internal fun CardListScreen( state: CardListState, navigateToNewCard: () -> Unit, + navigateToEditCard: (CreditCard) -> Unit, modifier: Modifier = Modifier, ) { Scaffold( @@ -110,6 +113,7 @@ internal fun CardListScreen( is CreditCardUiState.Many -> { ManyCardScreen( state = cards, + navigateToEditCard = navigateToEditCard, modifier = Modifier.padding(innerPadding), ) } @@ -118,6 +122,7 @@ internal fun CardListScreen( OneCardScreen( state = cards, newCardAddContent = paymentCardAdd, + navigateToEditCard = navigateToEditCard, modifier = Modifier.padding(innerPadding), ) } @@ -152,6 +157,7 @@ private fun EmptyCardScreen( @Composable private fun ManyCardScreen( state: CreditCardUiState.Many, + navigateToEditCard: (CreditCard) -> Unit, modifier: Modifier = Modifier, ) { LazyColumn( @@ -167,6 +173,9 @@ private fun ManyCardScreen( } ) { PaymentCard( + modifier = Modifier.clickable { + navigateToEditCard(it) + }, cardInfo = it, ) } @@ -177,6 +186,7 @@ private fun ManyCardScreen( fun OneCardScreen( state: CreditCardUiState.One, newCardAddContent: @Composable () -> Unit, + navigateToEditCard: (CreditCard) -> Unit, modifier: Modifier = Modifier, ) { Column( @@ -185,6 +195,9 @@ fun OneCardScreen( .fillMaxSize(), ) { PaymentCard( + modifier = Modifier.clickable { + navigateToEditCard(state.creditCard) + }, cardInfo = state.creditCard ) Spacer(modifier = Modifier.padding(bottom = 32.dp)) @@ -240,6 +253,7 @@ private fun CardListScreenPreview( CardListScreen( state = state, navigateToNewCard = {}, + navigateToEditCard = {}, ) } } diff --git a/app/src/main/java/nextstep/payments/ui/common/model/CreditCard.kt b/app/src/main/java/nextstep/payments/ui/common/model/CreditCard.kt index f69fc5fd..4869442a 100644 --- a/app/src/main/java/nextstep/payments/ui/common/model/CreditCard.kt +++ b/app/src/main/java/nextstep/payments/ui/common/model/CreditCard.kt @@ -1,17 +1,17 @@ package nextstep.payments.ui.common.model +import android.os.Parcelable +import kotlinx.parcelize.Parcelize import nextstep.payments.ui.new_card.CardType import kotlin.text.filter -/** - * 이 클래스가 common/model 하위에 있는데, 더 적절한 위치가 어디일지 궁금합니다. - */ +@Parcelize data class CreditCard( val cardNumber: String = "", val expiredDate: String = "", val ownerName: String = "", val company: CardType = CardType.NOT_SELECTED, -) { +): Parcelable { fun formatExpiredDate(): String { val groups = expiredDate.filter { it.isDigit() }.chunked(2) return groups.joinToString(" / ") diff --git a/app/src/main/java/nextstep/payments/ui/mapper/CardMapper.kt b/app/src/main/java/nextstep/payments/ui/mapper/CardMapper.kt index 5d753079..6cadbfa8 100644 --- a/app/src/main/java/nextstep/payments/ui/mapper/CardMapper.kt +++ b/app/src/main/java/nextstep/payments/ui/mapper/CardMapper.kt @@ -40,3 +40,13 @@ fun CardTypeEntity.toUi(): CardType { CardTypeEntity.KB -> CardType.KB } } + +fun CreditCard.toEntity(): CardEntity { + return CardEntity( + cardNumber = cardNumber, + expiredDate = expiredDate, + ownerName = ownerName, + password = "", + company = company.toData(), + ) +} diff --git a/app/src/main/java/nextstep/payments/ui/new_card/CardType.kt b/app/src/main/java/nextstep/payments/ui/new_card/CardType.kt index 03b50dbb..b999d5b6 100644 --- a/app/src/main/java/nextstep/payments/ui/new_card/CardType.kt +++ b/app/src/main/java/nextstep/payments/ui/new_card/CardType.kt @@ -1,14 +1,17 @@ package nextstep.payments.ui.new_card +import android.os.Parcelable import androidx.annotation.ColorInt import androidx.annotation.DrawableRes +import kotlinx.parcelize.Parcelize import nextstep.payments.R +@Parcelize enum class CardType( val companyName: String, @param:DrawableRes val imageResource: Int, @param:ColorInt val color: Int, -) { +): Parcelable { NOT_SELECTED("", 0, 0xFF333333.toInt()), BC("BC카드", R.drawable.bc, 0xFFF04651.toInt()), SHINHAN("신한카드", R.drawable.shinhan, 0xFF0046FF.toInt()), diff --git a/app/src/main/java/nextstep/payments/ui/new_card/NewCardScreen.kt b/app/src/main/java/nextstep/payments/ui/new_card/NewCardScreen.kt index d372d71c..50670a7e 100644 --- a/app/src/main/java/nextstep/payments/ui/new_card/NewCardScreen.kt +++ b/app/src/main/java/nextstep/payments/ui/new_card/NewCardScreen.kt @@ -81,7 +81,15 @@ fun NewCardScreenRoot( val isAddEnabled by remember { derivedStateOf { - state.isValid(CardInputValidator) + !state.isCardValueSame(viewModel.originalState) && state.isValid(CardInputValidator) + } + } + + val topBarTitle = remember { + if (viewModel.originalState == NewCardState.EMPTY) { + context.getString(R.string.new_card_top_bar_title) + } else { + context.getString(R.string.edit_card_top_bar_title) } } @@ -89,6 +97,7 @@ fun NewCardScreenRoot( state = state, isAddEnabled = isAddEnabled, onAction = viewModel::onAction, + topBarTitle = topBarTitle, modifier = modifier, ) } @@ -98,6 +107,7 @@ internal fun NewCardScreen( state: NewCardState, isAddEnabled: Boolean, onAction: (NewCardAction) -> Unit, + topBarTitle: String, modifier: Modifier = Modifier, ) { val cardNumberTransformation = remember { @@ -116,6 +126,7 @@ internal fun NewCardScreen( onSaveClick = { onAction(NewCardAction.OnAddCardClick) }, + title = topBarTitle, isAddEnabled = isAddEnabled, ) }, @@ -291,6 +302,7 @@ private fun NewCardScreenPreview() { showBottomSheet = false, ), isAddEnabled = true, + topBarTitle = "카드 추가", onAction = { }, ) } diff --git a/app/src/main/java/nextstep/payments/ui/new_card/NewCardState.kt b/app/src/main/java/nextstep/payments/ui/new_card/NewCardState.kt index d807fb93..d632e073 100644 --- a/app/src/main/java/nextstep/payments/ui/new_card/NewCardState.kt +++ b/app/src/main/java/nextstep/payments/ui/new_card/NewCardState.kt @@ -14,4 +14,14 @@ data class NewCardState( cardInputValidator.isCardOwnerNameValid(ownerName) && cardInputValidator.isPasswordValid(password) } + + fun isCardValueSame(other: NewCardState): Boolean { + return cardNumber == other.cardNumber && expiredDate == other.expiredDate + && ownerName == other.ownerName && password == other.password + && cardType == other.cardType + } + + companion object { + val EMPTY = NewCardState() + } } diff --git a/app/src/main/java/nextstep/payments/ui/new_card/NewCardTopBar.kt b/app/src/main/java/nextstep/payments/ui/new_card/NewCardTopBar.kt index a3462748..16168228 100644 --- a/app/src/main/java/nextstep/payments/ui/new_card/NewCardTopBar.kt +++ b/app/src/main/java/nextstep/payments/ui/new_card/NewCardTopBar.kt @@ -21,10 +21,11 @@ fun NewCardTopBar( onBackClick: () -> Unit, onSaveClick: () -> Unit, isAddEnabled: Boolean, + title: String, modifier: Modifier = Modifier, ) { TopAppBar( - title = { Text("카드 추가") }, + title = { Text(title) }, navigationIcon = { IconButton(onClick = { onBackClick() }) { Icon( diff --git a/app/src/main/java/nextstep/payments/ui/new_card/NewCardViewModel.kt b/app/src/main/java/nextstep/payments/ui/new_card/NewCardViewModel.kt index 5abbdaae..850dbe64 100644 --- a/app/src/main/java/nextstep/payments/ui/new_card/NewCardViewModel.kt +++ b/app/src/main/java/nextstep/payments/ui/new_card/NewCardViewModel.kt @@ -1,5 +1,6 @@ package nextstep.payments.ui.new_card +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.channels.Channel @@ -10,35 +11,71 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import nextstep.payments.data.card.CardEntity import nextstep.payments.data.card.PaymentCardRepository +import nextstep.payments.ui.common.model.CreditCard import nextstep.payments.ui.mapper.toData +import nextstep.payments.ui.mapper.toEntity -class NewCardViewModel( +// Hilt가 아닌 기본 viewModel 함수를 이용해 ViewModel을 생성할 때, savedStateHandle와 디폴트 인자를 함께 사용하면 +// ViewModel factory가 SavedStateHandle만 받는 생성자를 찾지 못해 에러가 발생한다고 합니다. +// 그래서 savedStateHandle만 있는 생성자를 만들기 위해 JvmOverloads를 이용했습니다. +class NewCardViewModel @JvmOverloads constructor( + savedStateHandle: SavedStateHandle, private val cardRepository: PaymentCardRepository = PaymentCardRepository, ) : ViewModel() { - private val _cardState = MutableStateFlow(NewCardState()) - val cardState = _cardState.asStateFlow() - private val eventChannel = Channel { } val events = eventChannel.receiveAsFlow() + private val _cardState: MutableStateFlow + + val originalState: NewCardState + + init { + // 전달된 카드가 있다면 꺼내서 state에 저장하기 + val cardInfo = savedStateHandle.remove(CARD_INFO_KEY) + + originalState = if (cardInfo != null) { + // repository에서 비밀번호 알아내기 + val pw = cardRepository.getPassword(cardInfo.toEntity()) + + NewCardState( + cardNumber = cardInfo.cardNumber, + expiredDate = cardInfo.expiredDate, + ownerName = cardInfo.ownerName, + password = pw, + showBottomSheet = false, + cardType = cardInfo.company, + ) + } else { + NewCardState.EMPTY + } + _cardState = MutableStateFlow(originalState) + } + + val cardState = _cardState.asStateFlow() + fun onAction(action: NewCardAction) { when (action) { is NewCardAction.OnCartNumberChange -> { setCardNumber(action.cardNumber) } + is NewCardAction.OnExpiredDateChange -> { setExpiredDate(action.expiredDate) } + is NewCardAction.OnOwnerNameChange -> { setOwnerName(action.ownerName) } + is NewCardAction.OnPasswordChange -> { setPassword(action.password) } + NewCardAction.OnAddCardClick -> { addCard() } + NewCardAction.OnBackClick -> { // 현재 상황에서는 이러한 로직이 불필요해 보이지만, eventChannel이 꽉차서 send에 실패할 경우 재시도할 수 있도록 trySend 대신 send를 이용했습니다. // 하지만 trySend 대신 send를 이용할 경우 coroutineScope이 필요합니다. @@ -138,4 +175,8 @@ class NewCardViewModel( ) } } + + companion object { + const val CARD_INFO_KEY = "exist_card_info" + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4e556a57..5c4050f7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -12,4 +12,6 @@ 새로운 카드를 등록해주세요 추가 카드 등록에 실패했습니다. + 카드 추가 + 카드 변경 diff --git a/build.gradle.kts b/build.gradle.kts index c1e23bcf..0c9efb47 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,4 +2,5 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.jetbrains.kotlin.android) apply false + alias(libs.plugins.parcelize) apply false } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6a56750c..b4bf395a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -36,3 +36,4 @@ androidx-material-icons-extended = { group = "androidx.compose.material", name = android-application = { id = "com.android.application", version.ref = "agp" } jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } From 552d5a2b22011d75057f07d3768c44314e6210c2 Mon Sep 17 00:00:00 2001 From: DoTheBestMayB Date: Tue, 21 Oct 2025 22:21:09 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=EC=B9=B4=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- .../data/card/PaymentCardRepository.kt | 26 ++++++++------ .../payments/ui/new_card/NewCardEvent.kt | 2 ++ .../payments/ui/new_card/NewCardScreen.kt | 10 ++++-- .../payments/ui/new_card/NewCardViewModel.kt | 36 ++++++++++++------- app/src/main/res/values/strings.xml | 1 + 6 files changed, 50 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 0935f695..c5a15e09 100644 --- a/README.md +++ b/README.md @@ -15,4 +15,4 @@ - [x] 카드 목록에서 카드를 선택하면 카드 수정 화면으로 이동한다. - [x] 카드 수정 화면에서 변경사항이 발생하지 않으면 수정이 불가능하다. -- [ ] 카드가 수정되면 카드 목록 화면에 변경사항이 반영된다. +- [x] 카드가 수정되면 카드 목록 화면에 변경사항이 반영된다. diff --git a/app/src/main/java/nextstep/payments/data/card/PaymentCardRepository.kt b/app/src/main/java/nextstep/payments/data/card/PaymentCardRepository.kt index a56a38a8..ff4e0581 100644 --- a/app/src/main/java/nextstep/payments/data/card/PaymentCardRepository.kt +++ b/app/src/main/java/nextstep/payments/data/card/PaymentCardRepository.kt @@ -2,28 +2,32 @@ package nextstep.payments.data.card object PaymentCardRepository { - private val _cards = mutableListOf() + // key : CardNumber + private val _cards = linkedMapOf() val cards: List - get() = _cards.toList() + get() = _cards.toList().map { it.second } fun addCard(cardEntity: CardEntity): Boolean { // 카드를 등록하기 전에 이미 등록된 카드 번호인지 확인합니다. // LazyList에서 key를 카드번호로 설정해뒀는데, 중복된 key를 가진 데이터가 있으면 에러가 발생해서 추가했습니다. - if (_cards.any { it.cardNumber == cardEntity.cardNumber }) { + if (cardEntity.cardNumber in _cards) { return false } - _cards.add(cardEntity) + _cards[cardEntity.cardNumber] = cardEntity return true } - fun getPassword(target: CardEntity): String { - for (card in cards) { - // 카드 번호는 고유한 값이므로 카드 번호만 비교하도록 구현했습니다. - if (card.cardNumber == target.cardNumber) { - return card.password - } + fun editCard(originalCard: CardEntity, newCard: CardEntity): Boolean { + if (newCard.cardNumber in _cards) { + return false } + _cards.remove(originalCard.cardNumber) + _cards[newCard.cardNumber] = newCard + return true + } - return "" + fun getPassword(target: CardEntity): String { + // 카드 번호는 고유한 값이므로 카드 번호만 비교하도록 구현했습니다. + return _cards[target.cardNumber]?.password ?: "" } } diff --git a/app/src/main/java/nextstep/payments/ui/new_card/NewCardEvent.kt b/app/src/main/java/nextstep/payments/ui/new_card/NewCardEvent.kt index d420b6ae..d165d9ae 100644 --- a/app/src/main/java/nextstep/payments/ui/new_card/NewCardEvent.kt +++ b/app/src/main/java/nextstep/payments/ui/new_card/NewCardEvent.kt @@ -4,5 +4,7 @@ sealed interface NewCardEvent { data object CardAddSuccess : NewCardEvent data object CardAddFail : NewCardEvent + data object CardEditSuccess : NewCardEvent + data object CardEditFail : NewCardEvent data object NavigateBack : NewCardEvent } diff --git a/app/src/main/java/nextstep/payments/ui/new_card/NewCardScreen.kt b/app/src/main/java/nextstep/payments/ui/new_card/NewCardScreen.kt index 50670a7e..fe6940be 100644 --- a/app/src/main/java/nextstep/payments/ui/new_card/NewCardScreen.kt +++ b/app/src/main/java/nextstep/payments/ui/new_card/NewCardScreen.kt @@ -61,7 +61,7 @@ fun NewCardScreenRoot( ObserveAsEvents(viewModel.events) { event -> when (event) { - NewCardEvent.CardAddSuccess -> { + NewCardEvent.CardAddSuccess, NewCardEvent.CardEditSuccess, NewCardEvent.NavigateBack -> { navigateToCardList() } @@ -73,8 +73,12 @@ fun NewCardScreenRoot( ).show() } - NewCardEvent.NavigateBack -> { - navigateToCardList() + NewCardEvent.CardEditFail -> { + Toast.makeText( + context, + context.getString(R.string.card_list_edit_card_fail), + Toast.LENGTH_SHORT + ).show() } } } diff --git a/app/src/main/java/nextstep/payments/ui/new_card/NewCardViewModel.kt b/app/src/main/java/nextstep/payments/ui/new_card/NewCardViewModel.kt index 850dbe64..18c53c34 100644 --- a/app/src/main/java/nextstep/payments/ui/new_card/NewCardViewModel.kt +++ b/app/src/main/java/nextstep/payments/ui/new_card/NewCardViewModel.kt @@ -52,6 +52,13 @@ class NewCardViewModel @JvmOverloads constructor( _cardState = MutableStateFlow(originalState) } + private val originalEntity = CardEntity( + originalState.cardNumber, + originalState.expiredDate, + originalState.ownerName, + originalState.password, + originalState.cardType.toData(), + ) val cardState = _cardState.asStateFlow() fun onAction(action: NewCardAction) { @@ -145,20 +152,25 @@ class NewCardViewModel @JvmOverloads constructor( CardInputValidator.isCardOwnerNameValid(_cardState.value.ownerName) && CardInputValidator.isPasswordValid(_cardState.value.password) ) { - val isSuccess = cardRepository.addCard( - CardEntity( - cardNumber = _cardState.value.cardNumber, - expiredDate = _cardState.value.expiredDate, - ownerName = _cardState.value.ownerName, - password = _cardState.value.password, - company = _cardState.value.cardType.toData(), - ) + val cardEntity = CardEntity( + cardNumber = _cardState.value.cardNumber, + expiredDate = _cardState.value.expiredDate, + ownerName = _cardState.value.ownerName, + password = _cardState.value.password, + company = _cardState.value.cardType.toData(), ) - - val event = if (isSuccess) { - NewCardEvent.CardAddSuccess + val event = if (originalState != NewCardState.EMPTY) { + if (cardRepository.editCard(originalEntity, cardEntity)) { + NewCardEvent.CardEditSuccess + } else { + NewCardEvent.CardEditFail + } } else { - NewCardEvent.CardAddFail + if (cardRepository.addCard(cardEntity)) { + NewCardEvent.CardAddSuccess + } else { + NewCardEvent.CardAddFail + } } viewModelScope.launch { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5c4050f7..96ab9191 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -14,4 +14,5 @@ 카드 등록에 실패했습니다. 카드 추가 카드 변경 + 카드 변경에 실패했습니다. From 4fe5cc22be0f62efc0174c2d5780538633821815 Mon Sep 17 00:00:00 2001 From: DoTheBestMayB Date: Tue, 21 Oct 2025 23:45:38 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20=EC=B9=B4=EB=93=9C=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=EB=A5=BC=20=EB=B3=80=EA=B2=BD=ED=95=B4=EB=8F=84,=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=EB=A5=BC=20=EB=93=B1=EB=A1=9D=ED=95=9C=20?= =?UTF-8?q?=EC=88=9C=EC=84=9C=EA=B0=80=20=EC=9C=A0=EC=A7=80=EB=90=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/payments/data/card/CardEntity.kt | 3 ++ .../data/card/PaymentCardRepository.kt | 46 +++++++++++++------ .../payments/ui/common/model/CreditCard.kt | 2 + .../nextstep/payments/ui/mapper/CardMapper.kt | 10 +--- .../payments/ui/new_card/NewCardState.kt | 3 ++ .../payments/ui/new_card/NewCardViewModel.kt | 14 ++---- 6 files changed, 44 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/nextstep/payments/data/card/CardEntity.kt b/app/src/main/java/nextstep/payments/data/card/CardEntity.kt index 5ea9ca58..d7dde8fb 100644 --- a/app/src/main/java/nextstep/payments/data/card/CardEntity.kt +++ b/app/src/main/java/nextstep/payments/data/card/CardEntity.kt @@ -1,6 +1,9 @@ package nextstep.payments.data.card +import java.util.UUID + data class CardEntity( + val id: UUID = UUID.randomUUID(), val cardNumber: String = "", val expiredDate: String = "", val ownerName: String = "", diff --git a/app/src/main/java/nextstep/payments/data/card/PaymentCardRepository.kt b/app/src/main/java/nextstep/payments/data/card/PaymentCardRepository.kt index ff4e0581..d8703cea 100644 --- a/app/src/main/java/nextstep/payments/data/card/PaymentCardRepository.kt +++ b/app/src/main/java/nextstep/payments/data/card/PaymentCardRepository.kt @@ -1,33 +1,49 @@ package nextstep.payments.data.card +import java.util.UUID + object PaymentCardRepository { - // key : CardNumber - private val _cards = linkedMapOf() + private val cardById = linkedMapOf() val cards: List - get() = _cards.toList().map { it.second } + get() = cardById.values.toList() + + private val idByCardNumber = hashMapOf() - fun addCard(cardEntity: CardEntity): Boolean { + /** + * 전달된 uuid가 이미 사용 중이라면 사용 중이지 않은 uuid로 변경해서 등록합니다. + */ + fun addCard(card: CardEntity): Boolean { // 카드를 등록하기 전에 이미 등록된 카드 번호인지 확인합니다. - // LazyList에서 key를 카드번호로 설정해뒀는데, 중복된 key를 가진 데이터가 있으면 에러가 발생해서 추가했습니다. - if (cardEntity.cardNumber in _cards) { + if (card.cardNumber in idByCardNumber) { return false } - _cards[cardEntity.cardNumber] = cardEntity + var id = card.id + while (id in cardById) { + id = UUID.randomUUID() + } + + cardById[id] = card.copy(id = id) + idByCardNumber[card.cardNumber] = id return true } - fun editCard(originalCard: CardEntity, newCard: CardEntity): Boolean { - if (newCard.cardNumber in _cards) { - return false + fun editCard(id: UUID, newCard: CardEntity): Boolean { + val oldCard = cardById[id] ?: return false + + if (newCard.cardNumber != oldCard.cardNumber) { + if (newCard.cardNumber in idByCardNumber) { + return false + } + idByCardNumber.remove(oldCard.cardNumber) + idByCardNumber[newCard.cardNumber] = id } - _cards.remove(originalCard.cardNumber) - _cards[newCard.cardNumber] = newCard + cardById[id] = newCard.copy(id = id) return true } - fun getPassword(target: CardEntity): String { - // 카드 번호는 고유한 값이므로 카드 번호만 비교하도록 구현했습니다. - return _cards[target.cardNumber]?.password ?: "" + fun getPassword(id: UUID): String { + return cardById[id]?.password ?: "" } + } diff --git a/app/src/main/java/nextstep/payments/ui/common/model/CreditCard.kt b/app/src/main/java/nextstep/payments/ui/common/model/CreditCard.kt index 4869442a..f51e4c74 100644 --- a/app/src/main/java/nextstep/payments/ui/common/model/CreditCard.kt +++ b/app/src/main/java/nextstep/payments/ui/common/model/CreditCard.kt @@ -3,10 +3,12 @@ package nextstep.payments.ui.common.model import android.os.Parcelable import kotlinx.parcelize.Parcelize import nextstep.payments.ui.new_card.CardType +import java.util.UUID import kotlin.text.filter @Parcelize data class CreditCard( + val id: UUID = UUID.randomUUID(), val cardNumber: String = "", val expiredDate: String = "", val ownerName: String = "", diff --git a/app/src/main/java/nextstep/payments/ui/mapper/CardMapper.kt b/app/src/main/java/nextstep/payments/ui/mapper/CardMapper.kt index 6cadbfa8..456e5159 100644 --- a/app/src/main/java/nextstep/payments/ui/mapper/CardMapper.kt +++ b/app/src/main/java/nextstep/payments/ui/mapper/CardMapper.kt @@ -7,6 +7,7 @@ import nextstep.payments.ui.new_card.CardType fun CardEntity.toUi(): CreditCard = CreditCard( + id = id, cardNumber = cardNumber, expiredDate = expiredDate, ownerName = ownerName, @@ -41,12 +42,3 @@ fun CardTypeEntity.toUi(): CardType { } } -fun CreditCard.toEntity(): CardEntity { - return CardEntity( - cardNumber = cardNumber, - expiredDate = expiredDate, - ownerName = ownerName, - password = "", - company = company.toData(), - ) -} diff --git a/app/src/main/java/nextstep/payments/ui/new_card/NewCardState.kt b/app/src/main/java/nextstep/payments/ui/new_card/NewCardState.kt index d632e073..febeef8b 100644 --- a/app/src/main/java/nextstep/payments/ui/new_card/NewCardState.kt +++ b/app/src/main/java/nextstep/payments/ui/new_card/NewCardState.kt @@ -1,5 +1,7 @@ package nextstep.payments.ui.new_card +import java.util.UUID + data class NewCardState( val cardNumber: String = "", val expiredDate: String = "", @@ -7,6 +9,7 @@ data class NewCardState( val password: String = "", val showBottomSheet: Boolean = true, val cardType: CardType = CardType.NOT_SELECTED, + val id: UUID = UUID.randomUUID(), ) { fun isValid(cardInputValidator: CardInputValidator): Boolean { return cardInputValidator.isCardNumberValid(cardNumber) && diff --git a/app/src/main/java/nextstep/payments/ui/new_card/NewCardViewModel.kt b/app/src/main/java/nextstep/payments/ui/new_card/NewCardViewModel.kt index 18c53c34..80f55079 100644 --- a/app/src/main/java/nextstep/payments/ui/new_card/NewCardViewModel.kt +++ b/app/src/main/java/nextstep/payments/ui/new_card/NewCardViewModel.kt @@ -13,7 +13,6 @@ import nextstep.payments.data.card.CardEntity import nextstep.payments.data.card.PaymentCardRepository import nextstep.payments.ui.common.model.CreditCard import nextstep.payments.ui.mapper.toData -import nextstep.payments.ui.mapper.toEntity // Hilt가 아닌 기본 viewModel 함수를 이용해 ViewModel을 생성할 때, savedStateHandle와 디폴트 인자를 함께 사용하면 // ViewModel factory가 SavedStateHandle만 받는 생성자를 찾지 못해 에러가 발생한다고 합니다. @@ -36,7 +35,7 @@ class NewCardViewModel @JvmOverloads constructor( originalState = if (cardInfo != null) { // repository에서 비밀번호 알아내기 - val pw = cardRepository.getPassword(cardInfo.toEntity()) + val pw = cardRepository.getPassword(cardInfo.id) NewCardState( cardNumber = cardInfo.cardNumber, @@ -45,6 +44,7 @@ class NewCardViewModel @JvmOverloads constructor( password = pw, showBottomSheet = false, cardType = cardInfo.company, + id = cardInfo.id, ) } else { NewCardState.EMPTY @@ -52,13 +52,6 @@ class NewCardViewModel @JvmOverloads constructor( _cardState = MutableStateFlow(originalState) } - private val originalEntity = CardEntity( - originalState.cardNumber, - originalState.expiredDate, - originalState.ownerName, - originalState.password, - originalState.cardType.toData(), - ) val cardState = _cardState.asStateFlow() fun onAction(action: NewCardAction) { @@ -153,6 +146,7 @@ class NewCardViewModel @JvmOverloads constructor( CardInputValidator.isPasswordValid(_cardState.value.password) ) { val cardEntity = CardEntity( + id = originalState.id, cardNumber = _cardState.value.cardNumber, expiredDate = _cardState.value.expiredDate, ownerName = _cardState.value.ownerName, @@ -160,7 +154,7 @@ class NewCardViewModel @JvmOverloads constructor( company = _cardState.value.cardType.toData(), ) val event = if (originalState != NewCardState.EMPTY) { - if (cardRepository.editCard(originalEntity, cardEntity)) { + if (cardRepository.editCard(originalState.id, cardEntity)) { NewCardEvent.CardEditSuccess } else { NewCardEvent.CardEditFail From cfb8c7a2977509432db66b608769485a125f955e Mon Sep 17 00:00:00 2001 From: DoTheBestMayB Date: Tue, 21 Oct 2025 23:49:11 +0900 Subject: [PATCH 5/5] =?UTF-8?q?test:=20CardListScreen=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EC=97=90=20navigateToEditCard=20=ED=8C=8C=EB=9D=BC?= =?UTF-8?q?=EB=AF=B8=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/payments/ui/card_list/CardListScreenTest.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/androidTest/java/nextstep/payments/ui/card_list/CardListScreenTest.kt b/app/src/androidTest/java/nextstep/payments/ui/card_list/CardListScreenTest.kt index 30e84eb8..d084caff 100644 --- a/app/src/androidTest/java/nextstep/payments/ui/card_list/CardListScreenTest.kt +++ b/app/src/androidTest/java/nextstep/payments/ui/card_list/CardListScreenTest.kt @@ -19,6 +19,7 @@ class CardListScreenTest { cards = CreditCardUiState.Empty ), navigateToNewCard = {}, + navigateToEditCard = {}, ) } @@ -41,6 +42,7 @@ class CardListScreenTest { ) ), navigateToNewCard = {}, + navigateToEditCard = {}, ) } @@ -70,6 +72,7 @@ class CardListScreenTest { ) ), navigateToNewCard = {}, + navigateToEditCard = {}, ) }