Repository: next-step/android-payments
Pull request #110: 🚀 4단계 - 페이먼츠(카드 수정)
Author: login: mogunjoa
Created: 2025-03-11T10:19:39Z | Merged: 2025-03-15T12:40:28Z

### 🚀 4단계 - 페이먼츠(카드 수정)
- 카드 수정 기능을 구현한다.
  - 카드 목록에서 카드를 선택하면 카드 수정 화면으로 이동한다.
  - 카드 수정 화면에서 변경사항이 발생하지 않으면 수정이 불가능하다.
  - 카드가 수정되면 카드 목록 화면에 변경사항이 반영된다.

석준님 안녕하세요~ 4단계 미션이 완료되어 PR 요청드립니다.
요즘 이것저것 일정이 많아서 요청이 늦어졌네요 ㅠㅜ
이전 미션에서 주신 피드백 사항도 곰곰히 생각해보며 열심히 개선해보았습니다.

미션 수행중 궁금했던 사항이 있는데 답변주시면 감사하겠습니다!

**refactor: Navigation 객체 탐색에서 Route 탐색으로 수정 및 카드 목록 클리시 해당 데이터 NewCard…**  ee3132d
카드 수정 로직을 구성하기 위해 Card 객체에 cardId 속성을 추가하였습니다.
카드 목록의 추가되어 있는 카드를 클릭시 해당 카드 객체의 cardId를 카드 등록 화면으로 전달하여 카드 수정 로직을 구성하기 위함이었습니다.
그런데 여기서 기존 Navigation을 통해 화면을 이동하는 방식이 data object 객체를 활용하여 이동하는 로직으로 구성되어있는데 데이터를 넘기기 위해서는 route 방식으로 데이터를 넘기는 방식 밖에 찾지 못해 일단 route 방식으로 수정하여 cardId를 카드 등록 화면으로 넘겼습니다. 혹시 기존 객체를 활용하여 넘기는 방법이 있을가요? 아니면 더 좋은 방법이 있었을가요..?

**feat: NewCardScreen 카드 수정 기능 추가 및 카드 추가 기능과 분기 처리** 3e02577e8eef0d073681347d5d21febc8df71aeb
NewCardScreen에 카드 수정과 카드 추가 기능을 cardId 값의 유무에 따라 분기처리하여 구현해보았습니다.
유사한 화면의 유사한 로직 처리라고 생각되어 따로 화면 분리를 하지않고 하나의 화면에서 두가지 로직을 구현하였는데 석준님이 보시기엔 어떠신지 궁금합니다.

일단 미션 진행 사항 중 궁금했던점은 여기까지입니다.
한번 코드보시고 또 피드백이 필요한 부분이 보인다면 말씀해주시면 감사하겠습니다!


Diff:
diff --git a/README.md b/README.md
index 24bbe230..3c9777d8 100644
--- a/README.md
+++ b/README.md
@@ -29,10 +29,16 @@
 - (선택사항) 카드사를 선택할 때 적절한 카드사 아이콘을 노출한다.
 - 카드사 선택 기능 구현시 FlowRow와 ModalBottomSheet를 활용한다.
 
-### Step3 - 개선 사항
+#### Step3 - 개선 사항
 - BankLogo 컴포넌트 Image 컴포넌트로 대체
 - AppContainer, PaymentCard Size 상수로 추출 및 적용
 - NewCardScreen 등록 가능 유효성 체크 로직 ViewModel로 이동
 - BankSelectRow 1, 2번째 행 열 맞춤
 - bankType.bankImageRes!! > bankType.bankImageRes 되도록 로직 수정
 - BankBottomModalSheet 뒤로 가기 방지
+
+### Step4 - 페이먼츠(카드 수정)
+- 카드 수정 기능을 구현한다.
+  - 카드 목록에서 카드를 선택하면 카드 수정 화면으로 이동한다.
+  - 카드 수정 화면에서 변경사항이 발생하지 않으면 수정이 불가능하다.
+  - 카드가 수정되면 카드 목록 화면에 변경사항이 반영된다.
diff --git a/app/src/androidTest/java/nextstep/payments/CardListScreenTest.kt b/app/src/androidTest/java/nextstep/payments/CardListScreenTest.kt
index 6d212d81..1d92ba07 100644
--- a/app/src/androidTest/java/nextstep/payments/CardListScreenTest.kt
+++ b/app/src/androidTest/java/nextstep/payments/CardListScreenTest.kt
@@ -1,5 +1,6 @@
 package nextstep.payments
 
+import androidx.compose.ui.test.assert
 import androidx.compose.ui.test.filter
 import androidx.compose.ui.test.hasAnySibling
 import androidx.compose.ui.test.hasText
@@ -10,7 +11,7 @@ import androidx.compose.ui.test.onNodeWithContentDescription
 import androidx.compose.ui.test.onNodeWithText
 import nextstep.payments.data.model.Card
 import nextstep.payments.data.repository.PaymentCardsRepository
-import nextstep.payments.ui.BankType
+import nextstep.payments.ui.CardCompanyType
 import nextstep.payments.ui.screen.CardListScreen
 import org.junit.Rule
 import org.junit.Test
@@ -24,15 +25,15 @@ class CardListScreenTest {
         expiredDate = "1223",
         ownerName = "홍길동",
         password = "1234",
-        bankType = BankType.BC,
+        cardCompanyType = CardCompanyType.BC,
     )
 
     val card2 = Card(
         cardNumber = "1234567812345678",
         expiredDate = "1223",
         ownerName = "홀리물리",
-        password = "1234",
-        bankType = BankType.BC,
+        password = "1111",
+        cardCompanyType = CardCompanyType.BC,
     )
 
     fun registerCards(vararg cards: Card) {
@@ -43,6 +44,7 @@ class CardListScreenTest {
         composeTestRule.setContent {
             CardListScreen(
                 navigateToNewCard = {},
+                navigateToUpdateCard = {}
             )
         }
     }
@@ -72,11 +74,8 @@ class CardListScreenTest {
         )
 
         // 등록된 카드가 있을 때 카드가 화면에 노출되는지 확인
-        composeTestRule.onAllNodesWithText("홍길동")
-            .filter(hasAnySibling(hasText("1234")))
-            .filter(hasAnySibling(hasText("12 / 23")))
-            .onFirst()
-            .assertExists()
+        composeTestRule.onNodeWithText("홍길동")
+            .assert(hasText("1234") and hasText("12 / 23"))
     }
 
     @Test
diff --git a/app/src/androidTest/java/nextstep/payments/NewCardScreenTest.kt b/app/src/androidTest/java/nextstep/payments/NewCardScreenRegisterTest.kt
similarity index 95%
rename from app/src/androidTest/java/nextstep/payments/NewCardScreenTest.kt
rename to app/src/androidTest/java/nextstep/payments/NewCardScreenRegisterTest.kt
index 7acc47ba..5626e091 100644
--- a/app/src/androidTest/java/nextstep/payments/NewCardScreenTest.kt
+++ b/app/src/androidTest/java/nextstep/payments/NewCardScreenRegisterTest.kt
@@ -13,7 +13,7 @@ import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 
-class NewCardScreenTest {
+class NewCardScreenRegisterTest {
 
     @get:Rule
     val composeTestRule = createComposeRule()
@@ -22,7 +22,8 @@ class NewCardScreenTest {
     fun setUp() {
         composeTestRule.setContent {
             NewCardScreen(
-                navigateToCardList = {}
+                navigateToCardList = {},
+                cardId = null,
             )
         }
     }
@@ -151,7 +152,7 @@ class NewCardScreenTest {
 
     @Test
     fun 화면_진입시_은행_선택_바텀_시트가_호출된다() {
-        composeTestRule.onNodeWithContentDescription("bankBottomSheet")
+        composeTestRule.onNodeWithContentDescription("cardCompanyBottomSheet")
             .assertIsDisplayed()
     }
 
@@ -162,7 +163,7 @@ class NewCardScreenTest {
             .performClick()
 
         // then
-        composeTestRule.onNodeWithContentDescription("bankBottomSheet")
+        composeTestRule.onNodeWithContentDescription("cardCompanyBottomSheet")
             .assertDoesNotExist()
     }
 
diff --git a/app/src/androidTest/java/nextstep/payments/NewCardScreenUpdateTest.kt b/app/src/androidTest/java/nextstep/payments/NewCardScreenUpdateTest.kt
new file mode 100644
index 00000000..bd340f85
--- /dev/null
+++ b/app/src/androidTest/java/nextstep/payments/NewCardScreenUpdateTest.kt
@@ -0,0 +1,80 @@
+package nextstep.payments
+
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithContentDescription
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import nextstep.payments.data.model.Card
+import nextstep.payments.data.repository.PaymentCardsRepository
+import nextstep.payments.ui.CardCompanyType
+import nextstep.payments.ui.screen.NewCardScreen
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+class NewCardScreenUpdateTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    @Before
+    fun setUp() {
+        PaymentCardsRepository.clearCards()
+
+        PaymentCardsRepository.addCard(
+            Card(
+                cardId = "fdsafefefew-ewfwefwe-fewf",
+                cardNumber = "1234567812345678",
+                expiredDate = "1223",
+                ownerName = "홍길동",
+                password = "1234",
+                cardCompanyType = CardCompanyType.BC
+            )
+        )
+
+        composeTestRule.setContent {
+            NewCardScreen(
+                navigateToCardList = {},
+                cardId = "fdsafefefew-ewfwefwe-fewf",
+            )
+        }
+    }
+
+    @Test
+    fun 카드_아이디가_존재시_앱바_제목이_카드_수정으로_변경된다() {
+
+        composeTestRule.onNodeWithText("카드 수정")
+            .assertExists()
+    }
+
+    @Test
+    fun 카드_아이디가_존재시_카드_정보가_화면에_노출된다() {
+
+        composeTestRule.onNodeWithText("카드 번호")
+            .assert(hasText("1234 - 5678 - 1234 - 5678"))
+
+        composeTestRule.onNodeWithText("만료일")
+            .assert(hasText("12 / 23"))
+
+        composeTestRule.onNodeWithText("카드 소유자 이름(선택)")
+            .assert(hasText("홍길동"))
+
+        composeTestRule.onNodeWithText("비밀번호")
+            .assert(hasText("••••"))
+
+        composeTestRule.onNodeWithText("BC카드")
+            .assertExists()
+    }
+
+    @Test
+    fun 카드_수정시_변경사항이_없으면_수정_불가_스낵바를_호출한다() {
+        composeTestRule.onNodeWithContentDescription("addCardCheck")
+            .performClick()
+
+        // then
+        composeTestRule.onNodeWithContentDescription("validateSnackbar")
+            .assertIsDisplayed()
+    }
+}
diff --git a/app/src/main/java/nextstep/payments/data/model/Card.kt b/app/src/main/java/nextstep/payments/data/model/Card.kt
index 988aa07d..b196c41e 100644
--- a/app/src/main/java/nextstep/payments/data/model/Card.kt
+++ b/app/src/main/java/nextstep/payments/data/model/Card.kt
@@ -1,11 +1,15 @@
 package nextstep.payments.data.model
 
-import nextstep.payments.ui.BankType
+import kotlinx.serialization.Serializable
+import nextstep.payments.ui.CardCompanyType
+import java.util.UUID
 
+@Serializable
 data class Card(
+    val cardId: String = UUID.randomUUID().toString(),
     val cardNumber: String,
     val expiredDate: String,
     val ownerName: String,
     val password: String,
-    val bankType: BankType,
+    val cardCompanyType: CardCompanyType,
 )
diff --git a/app/src/main/java/nextstep/payments/data/repository/PaymentCardsRepository.kt b/app/src/main/java/nextstep/payments/data/repository/PaymentCardsRepository.kt
index dfb1a018..2946a8ef 100644
--- a/app/src/main/java/nextstep/payments/data/repository/PaymentCardsRepository.kt
+++ b/app/src/main/java/nextstep/payments/data/repository/PaymentCardsRepository.kt
@@ -11,7 +11,18 @@ object PaymentCardsRepository {
         _cards.add(card)
     }
 
+    fun updateCard(updatedCard: Card) {
+        val index = _cards.indexOfFirst { it.cardId == updatedCard.cardId }
+        if (index != -1) {
+            _cards[index] = updatedCard
+        }
+    }
+
+    fun getCardById(cardId: String): Card? {
+        return _cards.find { it.cardId == cardId }
+    }
+
     fun clearCards() {
         _cards.clear()
     }
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/nextstep/payments/ui/BankType.kt b/app/src/main/java/nextstep/payments/ui/BankType.kt
deleted file mode 100644
index b567389c..00000000
--- a/app/src/main/java/nextstep/payments/ui/BankType.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-package nextstep.payments.ui
-
-import androidx.annotation.DrawableRes
-import androidx.annotation.StringRes
-import androidx.compose.ui.graphics.Color
-import nextstep.payments.R
-import nextstep.payments.ui.theme.BcColor
-import nextstep.payments.ui.theme.DefaultBankColor
-import nextstep.payments.ui.theme.HanaColor
-import nextstep.payments.ui.theme.HyundaiColor
-import nextstep.payments.ui.theme.KBColor
-import nextstep.payments.ui.theme.KakaoColor
-import nextstep.payments.ui.theme.LotteColor
-import nextstep.payments.ui.theme.ShinhanColor
-import nextstep.payments.ui.theme.WooriColor
-
-enum class BankType(
-    @StringRes val bankNameResId: Int,
-    @DrawableRes val bankImageRes: Int?,
-    val bankThemeColor: Color
-) {
-    NOT_SELECTED(R.string.bank_not_select, null, DefaultBankColor),
-    BC(R.string.bank_bc, R.drawable.bc, BcColor),
-    SHINHAN(R.string.bank_shinhan, R.drawable.shinhan, ShinhanColor),
-    KAKAO(R.string.bank_kakao, R.drawable.kakao, KakaoColor),
-    HYUNDAI(R.string.bank_hyundai, R.drawable.hyundai, HyundaiColor),
-    WOORI(R.string.bank_woori, R.drawable.woori, WooriColor),
-    LOTTE(R.string.bank_lotte, R.drawable.lotte, LotteColor),
-    HANA(R.string.bank_hana, R.drawable.hana, HanaColor),
-    KB(R.string.bank_kb, R.drawable.kb, KBColor);
-
-    companion object {
-        fun getBankList(): List<BankType> = entries.filter { it != NOT_SELECTED }
-    }
-}
diff --git a/app/src/main/java/nextstep/payments/ui/CardCompanyType.kt b/app/src/main/java/nextstep/payments/ui/CardCompanyType.kt
new file mode 100644
index 00000000..0e3584f0
--- /dev/null
+++ b/app/src/main/java/nextstep/payments/ui/CardCompanyType.kt
@@ -0,0 +1,33 @@
+package nextstep.payments.ui
+
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import androidx.compose.ui.graphics.Color
+import nextstep.payments.R
+import nextstep.payments.ui.theme.BcColor
+import nextstep.payments.ui.theme.HanaColor
+import nextstep.payments.ui.theme.HyundaiColor
+import nextstep.payments.ui.theme.KBColor
+import nextstep.payments.ui.theme.KakaoColor
+import nextstep.payments.ui.theme.LotteColor
+import nextstep.payments.ui.theme.ShinhanColor
+import nextstep.payments.ui.theme.WooriColor
+
+enum class CardCompanyType(
+    @StringRes val cardCompanyNameResId: Int,
+    @DrawableRes val cardCompanyImageRes: Int?,
+    val cardCompanyThemeColor: Color
+) {
+    BC(R.string.card_company_bc, R.drawable.bc, BcColor),
+    SHINHAN(R.string.card_company_shinhan, R.drawable.shinhan, ShinhanColor),
+    KAKAO(R.string.card_company_kakao, R.drawable.kakao, KakaoColor),
+    HYUNDAI(R.string.card_company_hyundai, R.drawable.hyundai, HyundaiColor),
+    WOORI(R.string.card_company_woori, R.drawable.woori, WooriColor),
+    LOTTE(R.string.card_company_lotte, R.drawable.lotte, LotteColor),
+    HANA(R.string.card_company_hana, R.drawable.hana, HanaColor),
+    KB(R.string.card_company_kb, R.drawable.kb, KBColor);
+
+    companion object {
+        fun getBankList(): List<CardCompanyType> = entries
+    }
+}
diff --git a/app/src/main/java/nextstep/payments/ui/screen/CardApp.kt b/app/src/main/java/nextstep/payments/ui/screen/CardApp.kt
index f3981215..ccf07626 100644
--- a/app/src/main/java/nextstep/payments/ui/screen/CardApp.kt
+++ b/app/src/main/java/nextstep/payments/ui/screen/CardApp.kt
@@ -10,6 +10,7 @@ import androidx.compose.ui.tooling.preview.Preview
 import androidx.navigation.compose.NavHost
 import androidx.navigation.compose.composable
 import androidx.navigation.compose.rememberNavController
+import androidx.navigation.toRoute
 import nextstep.payments.ui.screen.navigation.CardRoute
 
 
@@ -33,15 +34,21 @@ fun CardApp(
             composable<CardRoute.CardList> {
                 CardListScreen(
                     navigateToNewCard = {
-                        navController.navigate(CardRoute.NewCard)
+                        navController.navigate(CardRoute.NewCard(null))
+                    },
+                    navigateToUpdateCard = { cardId ->
+                        navController.navigate(CardRoute.NewCard(cardId))
                     }
                 )
             }
-            composable<CardRoute.NewCard> {
+
+            composable<CardRoute.NewCard> { backStackEntry ->
+                val newCard: CardRoute.NewCard = backStackEntry.toRoute()
+                val cardId = newCard.cardId
+
                 NewCardScreen(
-                    navigateToCardList = {
-                        navController.navigateUp()
-                    }
+                    cardId = cardId,
+                    navigateToCardList = { navController.navigateUp() }
                 )
             }
         }
@@ -52,4 +59,4 @@ fun CardApp(
 @Composable
 private fun CardAppPreview() {
     CardApp()
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/nextstep/payments/ui/screen/CartListScreen.kt b/app/src/main/java/nextstep/payments/ui/screen/CartListScreen.kt
index a752bd92..5646731b 100644
--- a/app/src/main/java/nextstep/payments/ui/screen/CartListScreen.kt
+++ b/app/src/main/java/nextstep/payments/ui/screen/CartListScreen.kt
@@ -37,17 +37,19 @@ import androidx.compose.ui.unit.sp
 import androidx.lifecycle.viewmodel.compose.viewModel
 import nextstep.payments.R
 import nextstep.payments.data.model.Card
-import nextstep.payments.ui.BankType
+import nextstep.payments.ui.CardCompanyType
 import nextstep.payments.ui.CardUiState
 import nextstep.payments.ui.screen.component.CenterTopBar
 import nextstep.payments.ui.screen.component.PaymentCard
 import nextstep.payments.ui.theme.Dimensions
+import nextstep.payments.ui.theme.Dimensions.CardRatioDefaults
 import nextstep.payments.ui.viewmodel.CardListViewModel
 
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 fun CardListScreen(
     navigateToNewCard: () -> Unit,
+    navigateToUpdateCard: (String) -> Unit,
     modifier: Modifier = Modifier,
     viewModel: CardListViewModel = viewModel(),
 ) {
@@ -101,6 +103,9 @@ fun CardListScreen(
                 is CardUiState.One -> {
                     OneCardContainer(
                         card = cardsState.data,
+                        onCardClick = {
+                            navigateToUpdateCard(cardsState.data.cardId)
+                        },
                         onClick = {
                             navigateToNewCard()
                         }
@@ -109,7 +114,10 @@ fun CardListScreen(
 
                 is CardUiState.Many -> {
                     CardListContainer(
-                        cardList = cardsState.data
+                        cardList = cardsState.data,
+                        onCardClick = { cardId ->
+                            navigateToUpdateCard(cardId)
+                        }
                     )
                 }
             }
@@ -121,17 +129,21 @@ fun CardListScreen(
 private fun OneCardContainer(
     card: Card,
     onClick: () -> Unit,
+    onCardClick: () -> Unit,
     modifier: Modifier = Modifier,
 ) {
     Column(
         modifier = modifier,
     ) {
         PaymentCard(
-            bankName = stringResource(card.bankType.bankNameResId),
+            cardCompanyName = stringResource(card.cardCompanyType.cardCompanyNameResId),
             cardNumber = card.cardNumber,
             expiredDate = card.expiredDate,
             ownerName = card.ownerName,
-            cardColor = card.bankType.bankThemeColor,
+            cardColor = card.cardCompanyType.cardCompanyThemeColor,
+            modifier = Modifier.clickable(
+                onClick = onCardClick
+            )
         )
         Spacer(modifier = Modifier.height(36.dp))
         AddCardContainer(
@@ -143,19 +155,24 @@ private fun OneCardContainer(
 @Composable
 private fun CardListContainer(
     cardList: List<Card>,
+    onCardClick: (String) -> Unit,
+    modifier: Modifier = Modifier,
 ) {
     LazyColumn(
-        modifier = Modifier.padding(horizontal = 16.dp),
+        modifier = modifier.padding(horizontal = 16.dp),
         verticalArrangement = Arrangement.spacedBy(36.dp),
         contentPadding = PaddingValues(12.dp),
     ) {
         items(cardList) { card ->
             PaymentCard(
-                bankName = card.bankType.name,
+                cardCompanyName = card.cardCompanyType.name,
                 cardNumber = card.cardNumber,
                 expiredDate = card.expiredDate,
                 ownerName = card.ownerName,
-                cardColor = card.bankType.bankThemeColor,
+                cardColor = card.cardCompanyType.cardCompanyThemeColor,
+                modifier = Modifier.clickable(
+                    onClick = { onCardClick(card.cardId) }
+                )
             )
         }
     }
@@ -169,7 +186,7 @@ private fun AddCardContainer(
     Box(
         modifier = modifier
             .width(Dimensions.CardWidthDefaults)
-            .aspectRatio(52 / 31f)
+            .aspectRatio(CardRatioDefaults)
             .background(
                 color = Color(0xFFE5E5E5),
                 shape = RoundedCornerShape(5.dp),
@@ -200,8 +217,9 @@ private fun OneCardContainerPreview() {
             expiredDate = "1234",
             ownerName = "홍길동",
             password = "12421412",
-            bankType = BankType.BC,
+            cardCompanyType = CardCompanyType.BC,
         ),
+        onCardClick = {},
         onClick = {},
     )
 }
@@ -216,23 +234,24 @@ private fun CardListConatinerPreview() {
                 expiredDate = "1234",
                 ownerName = "홍길동",
                 password = "12421412",
-                bankType = BankType.BC
+                cardCompanyType = CardCompanyType.BC
             ),
             Card(
                 cardNumber = "1234-5678-1234-5678",
                 expiredDate = "1234",
                 ownerName = "홍길동",
                 password = "12421412",
-                bankType = BankType.BC
+                cardCompanyType = CardCompanyType.BC
             ),
             Card(
                 cardNumber = "1234-5678-1234-5678",
                 expiredDate = "1234",
                 ownerName = "홍길동",
                 password = "12421412",
-                bankType = BankType.BC
+                cardCompanyType = CardCompanyType.BC
             ),
-        )
+        ),
+        onCardClick = {},
     )
 }
 
@@ -240,6 +259,7 @@ private fun CardListConatinerPreview() {
 @Composable
 private fun CardListScreenPreview() {
     CardListScreen(
-        navigateToNewCard = {}
+        navigateToUpdateCard = {},
+        navigateToNewCard = {},
     )
 }
diff --git a/app/src/main/java/nextstep/payments/ui/screen/NewCardScreen.kt b/app/src/main/java/nextstep/payments/ui/screen/NewCardScreen.kt
index 5067c29b..30783cbb 100644
--- a/app/src/main/java/nextstep/payments/ui/screen/NewCardScreen.kt
+++ b/app/src/main/java/nextstep/payments/ui/screen/NewCardScreen.kt
@@ -33,7 +33,6 @@ import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.painter.Painter
-import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.semantics.contentDescription
@@ -46,9 +45,10 @@ import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import androidx.lifecycle.viewmodel.compose.viewModel
+import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.launch
 import nextstep.payments.R
-import nextstep.payments.ui.BankType
+import nextstep.payments.ui.CardCompanyType
 import nextstep.payments.ui.screen.component.NewCardTopBar
 import nextstep.payments.ui.screen.component.OutlinedInputTextField
 import nextstep.payments.ui.screen.component.PaymentCard
@@ -56,10 +56,12 @@ import nextstep.payments.ui.theme.Dimensions
 import nextstep.payments.ui.utils.CardNumberVisualTransformation
 import nextstep.payments.ui.utils.ExpiryDateVisualTransformation
 import nextstep.payments.ui.viewmodel.NewCardViewModel
+import nextstep.payments.ui.viewmodel.SaveState
 
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 fun NewCardScreen(
+    cardId: String?,
     navigateToCardList: () -> Unit,
     modifier: Modifier = Modifier,
     viewModel: NewCardViewModel = viewModel(),
@@ -70,8 +72,8 @@ fun NewCardScreen(
     val password by viewModel.password.collectAsStateWithLifecycle()
     val isSaveEnabled by viewModel.isSaveEnabled.collectAsStateWithLifecycle()
 
-    val selectedBank by viewModel.selectedBank.collectAsStateWithLifecycle()
-    var isBottomSheetVisible by remember { mutableStateOf(false) }
+    val selectedCardCompany by viewModel.selectedCardCompany.collectAsStateWithLifecycle()
+    var isBottomSheetVisible by remember { mutableStateOf(true) }
     var sheetState = rememberModalBottomSheetState(
         confirmValueChange = { newState ->
             newState != SheetValue.Hidden
@@ -80,29 +82,62 @@ fun NewCardScreen(
 
     // 스낵바 상태 저장
     val snackbarHostState = remember { SnackbarHostState() }
+    val coroutineScope = rememberCoroutineScope()
 
-    // 저장 가능 여부 유효성 체크
-    viewModel.setIsSaveEnabled()
+    // 스낵바 메시지
+    val snackbarMessage = if (cardId == null) {
+        stringResource(R.string.validate_snack_bar_message)
+    } else {
+        stringResource(R.string.validate_modify_snack_bar_message)
+    }
 
-    LaunchedEffect(selectedBank) {
-        if (selectedBank == BankType.NOT_SELECTED) {
-            isBottomSheetVisible = true
+    // 앱바 타이틀
+    val appBarTitle = if (cardId == null) {
+        stringResource(R.string.card_add_app_bar_title)
+    } else {
+        stringResource(R.string.card_update_app_bar_title)
+    }
+
+    LaunchedEffect(Unit) {
+        if (cardId != null) {
+            viewModel.fetchCardById(cardId)
+            isBottomSheetVisible = false
+        }
+    }
+
+    LaunchedEffect(isBottomSheetVisible) {
+        if (isBottomSheetVisible) {
             sheetState.show()
         }
+    }
 
-        if (selectedBank != BankType.NOT_SELECTED) {
-            isBottomSheetVisible = false
-            sheetState.hide()
+    LaunchedEffect(Unit) {
+        viewModel.saveState.collectLatest { state ->
+            when (state) {
+                is SaveState.UpdateCard -> {
+                    navigateToCardList()
+                }
+
+                is SaveState.SaveNewCard -> {
+                    navigateToCardList()
+                }
+
+                is SaveState.ShowSnackbar -> {
+                    coroutineScope.launch {
+                        snackbarHostState.showSnackbar(snackbarMessage)
+                    }
+                }
+            }
         }
     }
 
     NewCardScreen(
+        appBarTitle = appBarTitle,
         cardNumber = cardNumber,
         expiredDate = expiredDate,
         ownerName = ownerName,
         password = password,
-        selectedBank = selectedBank,
-        isSaveEnabled = isSaveEnabled,
+        selectedCardCompany = selectedCardCompany,
         setCardNumber = viewModel::setCardNumber,
         setExpiredDate = viewModel::setExpiredDate,
         setOwnerName = viewModel::setOwnerName,
@@ -110,14 +145,7 @@ fun NewCardScreen(
         snackbarHostState = snackbarHostState,
         onBackCLick = navigateToCardList,
         onSaveClick = {
-            viewModel.addCard(
-                cardNumber = cardNumber,
-                expiredDate = expiredDate,
-                ownerName = ownerName,
-                password = password,
-                bankType = selectedBank
-            )
-            navigateToCardList()
+            viewModel.onSaveClick(cardId, isSaveEnabled)
         },
         modifier = modifier
     )
@@ -125,7 +153,13 @@ fun NewCardScreen(
     if (isBottomSheetVisible) {
         BankBottomModalSheet(
             sheetState = sheetState,
-            onBankClick = viewModel::setSelectedBank,
+            onBankClick = { selectedCardCompanyType ->
+                viewModel.setSelectedBank(selectedCardCompanyType)
+                coroutineScope.launch {
+                    sheetState.hide()
+                    isBottomSheetVisible = false
+                }
+            },
             modifier = Modifier.fillMaxWidth(),
         )
     }
@@ -133,12 +167,12 @@ fun NewCardScreen(
 
 @Composable
 private fun NewCardScreen(
+    appBarTitle: String,
     cardNumber: String,
     expiredDate: String,
     ownerName: String,
     password: String,
-    selectedBank: BankType,
-    isSaveEnabled: Boolean,
+    selectedCardCompany: CardCompanyType?,
     snackbarHostState: SnackbarHostState,
     setCardNumber: (String) -> Unit,
     setExpiredDate: (String) -> Unit,
@@ -148,22 +182,14 @@ private fun NewCardScreen(
     onSaveClick: () -> Unit,
     modifier: Modifier = Modifier,
 ) {
-    val context = LocalContext.current
-    val snackbarMessage = remember { context.getString(R.string.validate_snack_bar_message) }
-    val coroutineScope = rememberCoroutineScope()
 
     Scaffold(
         topBar = {
-            NewCardTopBar(onBackClick = onBackCLick, onSaveClick = {
-                if (isSaveEnabled) {
-                    onSaveClick()
-                    return@NewCardTopBar
-                }
-
-                coroutineScope.launch {
-                    snackbarHostState.showSnackbar(snackbarMessage)
-                }
-            })
+            NewCardTopBar(
+                appbarTitle = appBarTitle,
+                onBackClick = onBackCLick,
+                onSaveClick = onSaveClick,
+            )
         },
         snackbarHost = {
             SnackbarHost(
@@ -182,13 +208,22 @@ private fun NewCardScreen(
         ) {
             Spacer(modifier = Modifier.height(14.dp))
 
-            PaymentCard(
-                bankName = stringResource(selectedBank.bankNameResId),
-                cardNumber = cardNumber,
-                expiredDate = expiredDate,
-                ownerName = ownerName,
-                cardColor = selectedBank.bankThemeColor,
-            )
+            if (selectedCardCompany != null) {
+                PaymentCard(
+                    cardCompanyName = stringResource(selectedCardCompany.cardCompanyNameResId),
+                    cardNumber = cardNumber,
+                    expiredDate = expiredDate,
+                    ownerName = ownerName,
+                    cardColor = selectedCardCompany.cardCompanyThemeColor,
+                )
+            } else {
+                PaymentCard(
+                    cardCompanyName = "00",
+                    cardNumber = cardNumber,
+                    expiredDate = expiredDate,
+                    ownerName = ownerName,
+                )
+            }
 
             Spacer(modifier = Modifier.height(10.dp))
 
@@ -296,12 +331,12 @@ private fun PasswordInputField(
 @Composable
 private fun BankBottomModalSheet(
     sheetState: SheetState,
-    onBankClick: (BankType) -> Unit,
+    onBankClick: (CardCompanyType) -> Unit,
     modifier: Modifier = Modifier,
 ) {
     ModalBottomSheet(
         modifier = modifier.semantics {
-            contentDescription = "bankBottomSheet"
+            contentDescription = "cardCompanyBottomSheet"
         },
         sheetState = sheetState,
         onDismissRequest = {},
@@ -314,8 +349,8 @@ private fun BankBottomModalSheet(
 
             // 은행 리스트
             BankSelectRow(
-                onBankClick = { bankType ->
-                    onBankClick(bankType)
+                onBankClick = { cardCompanyType ->
+                    onBankClick(cardCompanyType)
                 },
             )
         }
@@ -325,10 +360,10 @@ private fun BankBottomModalSheet(
 @OptIn(ExperimentalLayoutApi::class)
 @Composable
 private fun BankSelectRow(
-    onBankClick: (BankType) -> Unit,
+    onBankClick: (CardCompanyType) -> Unit,
     modifier: Modifier = Modifier,
 ) {
-    val bankList = BankType.getBankList()
+    val cardCompanyList = CardCompanyType.getBankList()
 
     FlowRow(
         modifier = modifier
@@ -338,18 +373,19 @@ private fun BankSelectRow(
         verticalArrangement = Arrangement.spacedBy(23.dp),
         maxItemsInEachRow = 4
     ) {
-        bankList.forEach { bankType ->
+        cardCompanyList.forEach { cardCompanyType ->
 
-            if (bankType.bankImageRes == null) {
+            if (cardCompanyType.cardCompanyImageRes == null) {
                 return@forEach
             }
 
             BankItem(
-                bankName = stringResource(bankType.bankNameResId),
-                bankImage = painterResource(bankType.bankImageRes),
-                modifier = modifier.width(80.dp)
+                cardCompanyName = stringResource(cardCompanyType.cardCompanyNameResId),
+                cardCompanyImage = painterResource(cardCompanyType.cardCompanyImageRes),
+                modifier = modifier
+                    .width(80.dp)
                     .clickable(
-                        onClick = { onBankClick(bankType) },
+                        onClick = { onBankClick(cardCompanyType) },
                         indication = null,
                         interactionSource = remember { MutableInteractionSource() }
                     )
@@ -360,8 +396,8 @@ private fun BankSelectRow(
 
 @Composable
 private fun BankItem(
-    bankName: String,
-    bankImage: Painter,
+    cardCompanyName: String,
+    cardCompanyImage: Painter,
     modifier: Modifier = Modifier,
 ) {
     Column(
@@ -369,13 +405,13 @@ private fun BankItem(
         modifier = modifier
     ) {
         Image(
-            painter = bankImage,
+            painter = cardCompanyImage,
             contentDescription = "Bank Logo",
             modifier = Modifier.size(Dimensions.LogoDefaults),
         )
         Spacer(modifier = Modifier.height(10.dp))
         Text(
-            text = "${bankName}카드",
+            text = "${cardCompanyName}카드",
             fontSize = 16.sp,
             fontWeight = FontWeight.W500,
         )
@@ -386,8 +422,8 @@ private fun BankItem(
 @Composable
 private fun BankItemPreview() {
     BankItem(
-        bankName = "BC",
-        bankImage = painterResource(R.drawable.bc),
+        cardCompanyName = "BC",
+        cardCompanyImage = painterResource(R.drawable.bc),
     )
 }
 
@@ -440,6 +476,7 @@ private fun PasswordInputFieldPreview() {
 @Composable
 private fun StatefulNewCardScreenPreview() {
     NewCardScreen(
+        cardId = null,
         viewModel = NewCardViewModel().apply {
             setCardNumber("1234567812345678")
             setExpiredDate("12 / 34")
@@ -454,12 +491,12 @@ private fun StatefulNewCardScreenPreview() {
 @Composable
 private fun StatelessNewCardScreenPreView() {
     NewCardScreen(
+        appBarTitle = "카드 추가",
         cardNumber = "1234567812345678",
         expiredDate = "12 / 34",
         ownerName = "홍길동",
         password = "1234",
-        isSaveEnabled = true,
-        selectedBank = BankType.BC,
+        selectedCardCompany = CardCompanyType.BC,
         snackbarHostState = SnackbarHostState(),
         setCardNumber = {},
         setExpiredDate = {},
diff --git a/app/src/main/java/nextstep/payments/ui/screen/component/NewTopBar.kt b/app/src/main/java/nextstep/payments/ui/screen/component/NewTopBar.kt
index c72f74e4..037514e6 100644
--- a/app/src/main/java/nextstep/payments/ui/screen/component/NewTopBar.kt
+++ b/app/src/main/java/nextstep/payments/ui/screen/component/NewTopBar.kt
@@ -17,14 +17,17 @@ import nextstep.payments.R
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 fun NewCardTopBar(
+    appbarTitle: String,
     onBackClick: () -> Unit,
     onSaveClick: () -> Unit,
     modifier: Modifier = Modifier,
 ) {
     TopAppBar(
-        title = { Text(
-            stringResource(R.string.card_add_app_bar_title)
-        ) },
+        title = {
+            Text(
+                text = appbarTitle
+            )
+        },
         navigationIcon = {
             IconButton(onClick = { onBackClick() }) {
                 Icon(
@@ -49,6 +52,7 @@ fun NewCardTopBar(
 @Composable
 private fun NewCardTopBarPreview() {
     NewCardTopBar(
+        appbarTitle = "카드 추가",
         onBackClick = { },
         onSaveClick = { },
     )
diff --git a/app/src/main/java/nextstep/payments/ui/screen/component/PaymentCard.kt b/app/src/main/java/nextstep/payments/ui/screen/component/PaymentCard.kt
index 8e82eae9..dfd25684 100644
--- a/app/src/main/java/nextstep/payments/ui/screen/component/PaymentCard.kt
+++ b/app/src/main/java/nextstep/payments/ui/screen/component/PaymentCard.kt
@@ -26,10 +26,11 @@ import androidx.compose.ui.unit.sp
 import nextstep.payments.ui.utils.TextFormatUtil
 import nextstep.payments.R
 import nextstep.payments.ui.theme.Dimensions
+import nextstep.payments.ui.theme.Dimensions.CardRatioDefaults
 
 @Composable
 fun PaymentCard(
-    bankName: String,
+    cardCompanyName: String,
     cardNumber: String,
     expiredDate: String,
     ownerName: String,
@@ -41,7 +42,7 @@ fun PaymentCard(
         modifier = modifier
             .shadow(8.dp)
             .width(Dimensions.CardWidthDefaults)
-            .aspectRatio(52 / 31f)
+            .aspectRatio(CardRatioDefaults)
             .background(
                 color = cardColor,
                 shape = RoundedCornerShape(5.dp),
@@ -51,7 +52,7 @@ fun PaymentCard(
             modifier = Modifier.padding(horizontal = 14.dp)
         ) {
             Text(
-                text = "${bankName}카드",
+                text = "${cardCompanyName}카드",
                 fontSize = 12.sp,
                 fontWeight = FontWeight.W500,
                 color = Color.White
@@ -182,7 +183,7 @@ private fun DividerPreview() {
 @Composable
 private fun PaymentCardPreview() {
     PaymentCard(
-        bankName = "신한",
+        cardCompanyName = "신한",
         cardNumber = "1234123412341324",
         expiredDate = "1234",
         ownerName = "홍길동",
diff --git a/app/src/main/java/nextstep/payments/ui/screen/navigation/NavigationRoute.kt b/app/src/main/java/nextstep/payments/ui/screen/navigation/NavigationRoute.kt
index 6222fb79..4badfb11 100644
--- a/app/src/main/java/nextstep/payments/ui/screen/navigation/NavigationRoute.kt
+++ b/app/src/main/java/nextstep/payments/ui/screen/navigation/NavigationRoute.kt
@@ -2,10 +2,13 @@ package nextstep.payments.ui.screen.navigation
 
 import kotlinx.serialization.Serializable
 
+@Serializable
 sealed class CardRoute {
+
     @Serializable
     data object CardList : CardRoute()
 
     @Serializable
-    data object NewCard : CardRoute()
-}
\ No newline at end of file
+    data class NewCard(val cardId: String?) : CardRoute()
+}
+
diff --git a/app/src/main/java/nextstep/payments/ui/theme/Dimensions.kt b/app/src/main/java/nextstep/payments/ui/theme/Dimensions.kt
index 6ddfbe7e..775e90b9 100644
--- a/app/src/main/java/nextstep/payments/ui/theme/Dimensions.kt
+++ b/app/src/main/java/nextstep/payments/ui/theme/Dimensions.kt
@@ -4,5 +4,6 @@ import androidx.compose.ui.unit.dp
 
 object Dimensions {
     val CardWidthDefaults = 208.dp
+    val CardRatioDefaults = 52 / 31f
     val LogoDefaults = 37.dp
 }
diff --git a/app/src/main/java/nextstep/payments/ui/viewmodel/NewCardViewModel.kt b/app/src/main/java/nextstep/payments/ui/viewmodel/NewCardViewModel.kt
index 5dc1244c..ed8aed6c 100644
--- a/app/src/main/java/nextstep/payments/ui/viewmodel/NewCardViewModel.kt
+++ b/app/src/main/java/nextstep/payments/ui/viewmodel/NewCardViewModel.kt
@@ -1,12 +1,23 @@
 package nextstep.payments.ui.viewmodel
 
 import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharedFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.launch
 import nextstep.payments.data.model.Card
 import nextstep.payments.data.repository.PaymentCardsRepository
-import nextstep.payments.ui.BankType
+import nextstep.payments.ui.CardCompanyType
+
+sealed class SaveState {
+    data object UpdateCard : SaveState()
+    data object SaveNewCard : SaveState()
+    data object ShowSnackbar : SaveState()
+}
 
 class NewCardViewModel(
     private val paymentRepsoitory: PaymentCardsRepository = PaymentCardsRepository,
@@ -24,11 +35,26 @@ class NewCardViewModel(
     private val _password = MutableStateFlow("")
     val password: StateFlow<String> = _password.asStateFlow()
 
+    private val _selectedCardCompany = MutableStateFlow<CardCompanyType?>(null)
+    val selectedCardCompany: StateFlow<CardCompanyType?> = _selectedCardCompany.asStateFlow()
+
     private val _isSaveEnabled = MutableStateFlow(false)
     val isSaveEnabled: StateFlow<Boolean> = _isSaveEnabled.asStateFlow()
 
-    private val _selectedBank = MutableStateFlow(BankType.NOT_SELECTED)
-    val selectedBank: StateFlow<BankType> = _selectedBank.asStateFlow()
+    private val _saveState = MutableSharedFlow<SaveState>(replay = 1)
+    val saveState: SharedFlow<SaveState> = _saveState
+
+    private var modifyCard: Card? = null
+
+    init {
+        viewModelScope.launch {
+            combine(cardNumber, expiredDate, password) { card, date, pass ->
+                card.length == 16 && date.length == 4 && pass.length == 4
+            }.collect { valid ->
+                _isSaveEnabled.value = valid
+            }
+        }
+    }
 
     fun setCardNumber(cardNumber: String) {
         _cardNumber.value = cardNumber
@@ -46,30 +72,107 @@ class NewCardViewModel(
         _password.value = password
     }
 
-    fun setIsSaveEnabled() {
-        _isSaveEnabled.value =
-            cardNumber.value.length == 16 && expiredDate.value.length == 4 && password.value.length == 4
+    fun setSelectedBank(cardCompanyType: CardCompanyType) {
+        _selectedCardCompany.value = cardCompanyType
     }
 
-    fun setSelectedBank(bankType: BankType) {
-        _selectedBank.value = bankType
+    fun fetchCardById(cardId: String) {
+        modifyCard = paymentRepsoitory.getCardById(cardId)
+
+        modifyCard?.let { card ->
+            _cardNumber.value = card.cardNumber
+            _expiredDate.value = card.expiredDate
+            _ownerName.value = card.ownerName
+            _password.value = card.password
+            _selectedCardCompany.value = card.cardCompanyType
+        }
     }
 
-    fun addCard(
+    fun onSaveClick(cardId: String?, isSaveEnabled: Boolean) {
+
+        // 저장이 불가능한 상태
+        if (!isSaveEnabled) {
+            _saveState.tryEmit(SaveState.ShowSnackbar)
+            return
+        }
+
+        // 카드 수정
+        if (cardId != null) {
+            updateCard(
+                cardId = cardId,
+                cardNumber = _cardNumber.value,
+                expiredDate = _expiredDate.value,
+                ownerName = _ownerName.value,
+                password = _password.value,
+                cardCompanyType = _selectedCardCompany.value
+            )
+
+            _saveState.tryEmit(SaveState.UpdateCard)
+            return
+        }
+
+        // 새로운 카드 추가
+        addCard(
+            cardNumber = _cardNumber.value,
+            expiredDate = _expiredDate.value,
+            ownerName = _ownerName.value,
+            password = _password.value,
+            cardCompanyType = _selectedCardCompany.value
+        )
+
+        _saveState.tryEmit(SaveState.SaveNewCard)
+    }
+
+    private fun addCard(
         cardNumber: String,
         expiredDate: String,
         ownerName: String,
         password: String,
-        bankType: BankType,
+        cardCompanyType: CardCompanyType?,
     ) {
+        if (cardCompanyType == null) {
+            return
+        }
+
         paymentRepsoitory.addCard(
             Card(
                 cardNumber = cardNumber,
                 expiredDate = expiredDate,
                 ownerName = ownerName,
                 password = password,
-                bankType = bankType
+                cardCompanyType = cardCompanyType
             )
         )
     }
+
+    private fun updateCard(
+        cardId: String,
+        cardNumber: String,
+        expiredDate: String,
+        ownerName: String,
+        password: String,
+        cardCompanyType: CardCompanyType?,
+    ) {
+        if (cardCompanyType == null) {
+            return
+        }
+
+        val updatedCard = Card(
+            cardId = cardId,
+            cardNumber = cardNumber,
+            expiredDate = expiredDate,
+            ownerName = ownerName,
+            password = password,
+            cardCompanyType = cardCompanyType
+        )
+
+        if (modifyCard == updatedCard) {
+            _saveState.tryEmit(SaveState.ShowSnackbar)
+            return
+        }
+
+        paymentRepsoitory.updateCard(
+            updatedCard = updatedCard
+        )
+    }
 }
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 62f60ecb..d6cbed0d 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -4,8 +4,10 @@
     <string name="app_bar_add_action_button">추가</string>
     <string name="card_app_title">"Payments"</string>
     <string name="card_add_app_bar_title">카드 추가</string>
+    <string name="card_update_app_bar_title">카드 수정</string>
     <string name="new_card_register_text">새로운 카드를 등록해주세요</string>
     <string name="validate_snack_bar_message">카드 정보를 확인해주세요.</string>
+    <string name="validate_modify_snack_bar_message">수정할 카드 정보를 확인해주세요.</string>
 
     <string name="register_card_icon_content_description">registerCardContainer</string>
     <string name="back_stack_icon_content_description">backStack</string>
@@ -25,13 +27,12 @@
 
     <string name="password_hidden_text">****</string>
 
-    <string name="bank_not_select">OO</string>
-    <string name="bank_bc">BC</string>
-    <string name="bank_shinhan">신한</string>
-    <string name="bank_kakao">카카오</string>
-    <string name="bank_hyundai">현대</string>
-    <string name="bank_woori">우리</string>
-    <string name="bank_lotte">롯데</string>
-    <string name="bank_hana">하나</string>
-    <string name="bank_kb">국민</string>
+    <string name="card_company_bc">BC</string>
+    <string name="card_company_shinhan">신한</string>
+    <string name="card_company_kakao">카카오</string>
+    <string name="card_company_hyundai">현대</string>
+    <string name="card_company_woori">우리</string>
+    <string name="card_company_lotte">롯데</string>
+    <string name="card_company_hana">하나</string>
+    <string name="card_company_kb">국민</string>
 </resources>


Reviews:
user: login: sjjeong
body: 4단계 미션 구현 잘 해주셨네요.
마지막 단계라서 도움이 될만한 피드백을 추가로 더 남겨봤습니다.
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-03-13T01:59:39Z
user: login: mogunjoa
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-14T04:22:52Z
user: login: mogunjoa
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-14T04:24:08Z
user: login: mogunjoa
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-14T06:01:38Z
user: login: mogunjoa
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-14T06:03:36Z
user: login: mogunjoa
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-14T07:40:40Z
user: login: sjjeong
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-14T14:40:44Z
user: login: sjjeong
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-14T14:42:07Z
user: login: sjjeong
body: 리뷰사항 잘 반영해주셨네요!
조금 더 개선하면 좋을 부분 코멘트 드렸습니다~
마지막 단계 미션이니 한번 더 Request change 보내드립니다 ㅎㅎ
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-03-14T14:57:25Z
user: login: mogunjoa
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-15T08:04:54Z
user: login: mogunjoa
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-15T08:06:52Z
user: login: mogunjoa
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-15T08:21:57Z
user: login: mogunjoa
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-15T08:30:03Z
user: login: sjjeong
body: 4단계 미션은 여기까지 진행하면 될 것 같아요 ㅎㅎ
고생하셨습니다!
state: APPROVED
author_association: NONE
submitted_at: 2025-03-15T12:40:22Z

Review comments:
diff_hunk: @@ -14,12 +13,11 @@ import nextstep.payments.ui.theme.LotteColor
 import nextstep.payments.ui.theme.ShinhanColor
 import nextstep.payments.ui.theme.WooriColor
 
-enum class BankType(
+enum class CardCompanyType(
path: app/src/main/java/nextstep/payments/ui/CardCompanyType.kt
user: login: sjjeong
body: 👍 
created_at: 2025-03-13T01:35:22Z
author_association: NONE
diff_hunk: @@ -2,10 +2,16 @@ package nextstep.payments.ui.screen.navigation
 
 import kotlinx.serialization.Serializable
 
-sealed class CardRoute {
path: app/src/main/java/nextstep/payments/ui/screen/navigation/NavigationRoute.kt
user: login: sjjeong
body: String으로 route를 추가하신 이유가 어떤 것일까요?
이렇게 되면 타입 안정성을 보장할 수 없어서 좋지 않은 코드라고 생각합니다.
cardId때문에 이렇게 하신 거라면 이 문서를 참고해보면 좋겠네요
https://developer.android.com/guide/navigation/design#compose-arguments
created_at: 2025-03-13T01:46:07Z
author_association: NONE
diff_hunk: @@ -125,19 +154,26 @@ fun NewCardScreen(
     if (isBottomSheetVisible) {
         BankBottomModalSheet(
             sheetState = sheetState,
-            onBankClick = viewModel::setSelectedBank,
+            onBankClick = { selectedCardCompanyType ->
+                viewModel.setSelectedBank(selectedCardCompanyType)
+                coroutineScope.launch {
+                    sheetState.hide()
+                    isBottomSheetVisible = false
+                }
+            },
             modifier = Modifier.fillMaxWidth(),
         )
     }
 }
 
 @Composable
 private fun NewCardScreen(
+    cardId: String?,
     cardNumber: String,
     expiredDate: String,
     ownerName: String,
     password: String,
-    selectedBank: BankType,
+    selectedBank: CardCompanyType?,
path: app/src/main/java/nextstep/payments/ui/screen/NewCardScreen.kt
user: login: sjjeong
body: BankType이 CardCompanyType으로 변경되었으니 파라미터 이름도 변경되면 좋겠네요
created_at: 2025-03-13T01:48:24Z
author_association: NONE
diff_hunk: @@ -46,30 +62,74 @@ class NewCardViewModel(
         _password.value = password
     }
 
-    fun setIsSaveEnabled() {
-        _isSaveEnabled.value =
-            cardNumber.value.length == 16 && expiredDate.value.length == 4 && password.value.length == 4
-    }
-
-    fun setSelectedBank(bankType: BankType) {
-        _selectedBank.value = bankType
+    fun setSelectedBank(cardCompanyType: CardCompanyType) {
+        _selectedCardCompany.value = cardCompanyType
     }
 
     fun addCard(
         cardNumber: String,
         expiredDate: String,
         ownerName: String,
         password: String,
-        bankType: BankType,
+        cardCompanyType: CardCompanyType?,
     ) {
+        if (cardCompanyType == null) {
+            return
+        }
+
         paymentRepsoitory.addCard(
             Card(
                 cardNumber = cardNumber,
                 expiredDate = expiredDate,
                 ownerName = ownerName,
                 password = password,
-                bankType = bankType
+                cardCompanyType = cardCompanyType
             )
         )
     }
+
+    fun getCardById(cardId: String) {
+        modifyCard = paymentRepsoitory.getCardById(cardId)
+
+        modifyCard?.let { card ->
+            _cardNumber.value = card.cardNumber
+            _expiredDate.value = card.expiredDate
+            _ownerName.value = card.ownerName
+            _password.value = card.password
+            _selectedCardCompany.value = card.cardCompanyType
+        }
+    }
+
+    fun updateCard(
+        cardId: String,
+        cardNumber: String,
+        expiredDate: String,
+        ownerName: String,
+        password: String,
+        cardCompanyType: CardCompanyType?,
+    ): Boolean {
path: app/src/main/java/nextstep/payments/ui/viewmodel/NewCardViewModel.kt
user: login: sjjeong
body: MVVM이나 MVI 패턴에서 이렇게 함수에서 Boolean을 리턴하는게 좋은 패턴일까요??
Boolean을 리턴하지 않고 처리하려면 어떻게 하면 좋을까요?
created_at: 2025-03-13T01:51:21Z
author_association: NONE
diff_hunk: @@ -46,30 +62,74 @@ class NewCardViewModel(
         _password.value = password
     }
 
-    fun setIsSaveEnabled() {
-        _isSaveEnabled.value =
-            cardNumber.value.length == 16 && expiredDate.value.length == 4 && password.value.length == 4
-    }
-
-    fun setSelectedBank(bankType: BankType) {
-        _selectedBank.value = bankType
+    fun setSelectedBank(cardCompanyType: CardCompanyType) {
+        _selectedCardCompany.value = cardCompanyType
     }
 
     fun addCard(
         cardNumber: String,
         expiredDate: String,
         ownerName: String,
         password: String,
-        bankType: BankType,
+        cardCompanyType: CardCompanyType?,
     ) {
+        if (cardCompanyType == null) {
+            return
+        }
+
         paymentRepsoitory.addCard(
             Card(
                 cardNumber = cardNumber,
                 expiredDate = expiredDate,
                 ownerName = ownerName,
                 password = password,
-                bankType = bankType
+                cardCompanyType = cardCompanyType
             )
         )
     }
+
+    fun getCardById(cardId: String) {
path: app/src/main/java/nextstep/payments/ui/viewmodel/NewCardViewModel.kt
user: login: sjjeong
body: getCardById라는 이름은 Id로 찾은 Card를 반환하는 형태의 이름입니다.
get보다는 fetch같은 느낌의 이름이 더 좋지 않을까요?
created_at: 2025-03-13T01:53:53Z
author_association: NONE
diff_hunk: @@ -146,24 +182,40 @@ private fun NewCardScreen(
     setPassword: (String) -> Unit,
     onBackCLick: () -> Unit,
     onSaveClick: () -> Unit,
+    onUpdateClick: () -> Unit,
     modifier: Modifier = Modifier,
 ) {
     val context = LocalContext.current
     val snackbarMessage = remember { context.getString(R.string.validate_snack_bar_message) }
+
     val coroutineScope = rememberCoroutineScope()
 
+    val appBarTitle = if (cardId == null) {
+        stringResource(R.string.card_add_app_bar_title)
+    } else {
+        stringResource(R.string.card_update_app_bar_title)
+    }
+
     Scaffold(
         topBar = {
-            NewCardTopBar(onBackClick = onBackCLick, onSaveClick = {
-                if (isSaveEnabled) {
-                    onSaveClick()
-                    return@NewCardTopBar
-                }
-
-                coroutineScope.launch {
-                    snackbarHostState.showSnackbar(snackbarMessage)
-                }
-            })
+            NewCardTopBar(
+                appbarTitle = appBarTitle,
+                onBackClick = onBackCLick,
+                onSaveClick = {
+                    if (cardId != null && isSaveEnabled) {
+                        onUpdateClick()
+                        return@NewCardTopBar
+                    }
+
+                    if (isSaveEnabled) {
+                        onSaveClick()
+                        return@NewCardTopBar
+                    }
+
+                    coroutineScope.launch {
+                        snackbarHostState.showSnackbar(snackbarMessage)
+                    }
path: app/src/main/java/nextstep/payments/ui/screen/NewCardScreen.kt
user: login: sjjeong
body: 페이먼츠에서는 다룰 주제는 아니긴 하지만,
이런 로직들이 View에서 처리하는게 맞을까? 라는 생각이 듭니다.
ViewModel을 만드셨으니 ViewModel에서 처리 할 수 있는 방법으로 변경하면 좋을 것 같습니다.
created_at: 2025-03-13T01:58:00Z
author_association: NONE
diff_hunk: @@ -2,10 +2,16 @@ package nextstep.payments.ui.screen.navigation
 
 import kotlinx.serialization.Serializable
 
-sealed class CardRoute {
path: app/src/main/java/nextstep/payments/ui/screen/navigation/NavigationRoute.kt
user: login: mogunjoa
body: 오우 이런 부분이 있었군요! 제가 못찾았나봅니다ㅠㅜ 좋은 자료 감사합니다!!
created_at: 2025-03-14T04:22:51Z
author_association: NONE
diff_hunk: @@ -125,19 +154,26 @@ fun NewCardScreen(
     if (isBottomSheetVisible) {
         BankBottomModalSheet(
             sheetState = sheetState,
-            onBankClick = viewModel::setSelectedBank,
+            onBankClick = { selectedCardCompanyType ->
+                viewModel.setSelectedBank(selectedCardCompanyType)
+                coroutineScope.launch {
+                    sheetState.hide()
+                    isBottomSheetVisible = false
+                }
+            },
             modifier = Modifier.fillMaxWidth(),
         )
     }
 }
 
 @Composable
 private fun NewCardScreen(
+    cardId: String?,
     cardNumber: String,
     expiredDate: String,
     ownerName: String,
     password: String,
-    selectedBank: BankType,
+    selectedBank: CardCompanyType?,
path: app/src/main/java/nextstep/payments/ui/screen/NewCardScreen.kt
user: login: mogunjoa
body: 아이고 이부분을 놓쳤었군요. 수정하겠습니다!
created_at: 2025-03-14T04:24:08Z
author_association: NONE
diff_hunk: @@ -46,30 +62,74 @@ class NewCardViewModel(
         _password.value = password
     }
 
-    fun setIsSaveEnabled() {
-        _isSaveEnabled.value =
-            cardNumber.value.length == 16 && expiredDate.value.length == 4 && password.value.length == 4
-    }
-
-    fun setSelectedBank(bankType: BankType) {
-        _selectedBank.value = bankType
+    fun setSelectedBank(cardCompanyType: CardCompanyType) {
+        _selectedCardCompany.value = cardCompanyType
     }
 
     fun addCard(
         cardNumber: String,
         expiredDate: String,
         ownerName: String,
         password: String,
-        bankType: BankType,
+        cardCompanyType: CardCompanyType?,
     ) {
+        if (cardCompanyType == null) {
+            return
+        }
+
         paymentRepsoitory.addCard(
             Card(
                 cardNumber = cardNumber,
                 expiredDate = expiredDate,
                 ownerName = ownerName,
                 password = password,
-                bankType = bankType
+                cardCompanyType = cardCompanyType
             )
         )
     }
+
+    fun getCardById(cardId: String) {
+        modifyCard = paymentRepsoitory.getCardById(cardId)
+
+        modifyCard?.let { card ->
+            _cardNumber.value = card.cardNumber
+            _expiredDate.value = card.expiredDate
+            _ownerName.value = card.ownerName
+            _password.value = card.password
+            _selectedCardCompany.value = card.cardCompanyType
+        }
+    }
+
+    fun updateCard(
+        cardId: String,
+        cardNumber: String,
+        expiredDate: String,
+        ownerName: String,
+        password: String,
+        cardCompanyType: CardCompanyType?,
+    ): Boolean {
path: app/src/main/java/nextstep/payments/ui/viewmodel/NewCardViewModel.kt
user: login: mogunjoa
body: 아하 이게 함수에 반환값을 걸어버리게 되면 UI와 로직이 얽힐 수가 있겠군요. 좋지 않은 방법인 것 같습니다.
그럼 ViewModel에 선언하는 함수는 명령 함수 위주로 구성해야하는게 맞겠죠?
created_at: 2025-03-14T06:01:38Z
author_association: NONE
diff_hunk: @@ -46,30 +62,74 @@ class NewCardViewModel(
         _password.value = password
     }
 
-    fun setIsSaveEnabled() {
-        _isSaveEnabled.value =
-            cardNumber.value.length == 16 && expiredDate.value.length == 4 && password.value.length == 4
-    }
-
-    fun setSelectedBank(bankType: BankType) {
-        _selectedBank.value = bankType
+    fun setSelectedBank(cardCompanyType: CardCompanyType) {
+        _selectedCardCompany.value = cardCompanyType
     }
 
     fun addCard(
         cardNumber: String,
         expiredDate: String,
         ownerName: String,
         password: String,
-        bankType: BankType,
+        cardCompanyType: CardCompanyType?,
     ) {
+        if (cardCompanyType == null) {
+            return
+        }
+
         paymentRepsoitory.addCard(
             Card(
                 cardNumber = cardNumber,
                 expiredDate = expiredDate,
                 ownerName = ownerName,
                 password = password,
-                bankType = bankType
+                cardCompanyType = cardCompanyType
             )
         )
     }
+
+    fun getCardById(cardId: String) {
path: app/src/main/java/nextstep/payments/ui/viewmodel/NewCardViewModel.kt
user: login: mogunjoa
body: 함수명을 짓는게 생각보다 쉽지 않군요 ㅠㅜ 혹시 이런 부분에 대한 가이드라인이 따로 있을가요?
created_at: 2025-03-14T06:03:35Z
author_association: NONE
diff_hunk: @@ -146,24 +182,40 @@ private fun NewCardScreen(
     setPassword: (String) -> Unit,
     onBackCLick: () -> Unit,
     onSaveClick: () -> Unit,
+    onUpdateClick: () -> Unit,
     modifier: Modifier = Modifier,
 ) {
     val context = LocalContext.current
     val snackbarMessage = remember { context.getString(R.string.validate_snack_bar_message) }
+
     val coroutineScope = rememberCoroutineScope()
 
+    val appBarTitle = if (cardId == null) {
+        stringResource(R.string.card_add_app_bar_title)
+    } else {
+        stringResource(R.string.card_update_app_bar_title)
+    }
+
     Scaffold(
         topBar = {
-            NewCardTopBar(onBackClick = onBackCLick, onSaveClick = {
-                if (isSaveEnabled) {
-                    onSaveClick()
-                    return@NewCardTopBar
-                }
-
-                coroutineScope.launch {
-                    snackbarHostState.showSnackbar(snackbarMessage)
-                }
-            })
+            NewCardTopBar(
+                appbarTitle = appBarTitle,
+                onBackClick = onBackCLick,
+                onSaveClick = {
+                    if (cardId != null && isSaveEnabled) {
+                        onUpdateClick()
+                        return@NewCardTopBar
+                    }
+
+                    if (isSaveEnabled) {
+                        onSaveClick()
+                        return@NewCardTopBar
+                    }
+
+                    coroutineScope.launch {
+                        snackbarHostState.showSnackbar(snackbarMessage)
+                    }
path: app/src/main/java/nextstep/payments/ui/screen/NewCardScreen.kt
user: login: mogunjoa
body: 그렇군요. 최대한 비즈니스 로직 같은 경우에는 ViewModel로 빼는게 좋은 방법이군요. 한번 그렇게 수정해보겠습니다!
created_at: 2025-03-14T07:40:40Z
author_association: NONE
diff_hunk: @@ -46,30 +62,74 @@ class NewCardViewModel(
         _password.value = password
     }
 
-    fun setIsSaveEnabled() {
-        _isSaveEnabled.value =
-            cardNumber.value.length == 16 && expiredDate.value.length == 4 && password.value.length == 4
-    }
-
-    fun setSelectedBank(bankType: BankType) {
-        _selectedBank.value = bankType
+    fun setSelectedBank(cardCompanyType: CardCompanyType) {
+        _selectedCardCompany.value = cardCompanyType
     }
 
     fun addCard(
         cardNumber: String,
         expiredDate: String,
         ownerName: String,
         password: String,
-        bankType: BankType,
+        cardCompanyType: CardCompanyType?,
     ) {
+        if (cardCompanyType == null) {
+            return
+        }
+
         paymentRepsoitory.addCard(
             Card(
                 cardNumber = cardNumber,
                 expiredDate = expiredDate,
                 ownerName = ownerName,
                 password = password,
-                bankType = bankType
+                cardCompanyType = cardCompanyType
             )
         )
     }
+
+    fun getCardById(cardId: String) {
path: app/src/main/java/nextstep/payments/ui/viewmodel/NewCardViewModel.kt
user: login: sjjeong
body: 좋은 코드를 많이 보는 것 말고는 없는 것 같네요 ㅎㅎ
created_at: 2025-03-14T14:40:43Z
author_association: NONE
diff_hunk: @@ -46,30 +62,74 @@ class NewCardViewModel(
         _password.value = password
     }
 
-    fun setIsSaveEnabled() {
-        _isSaveEnabled.value =
-            cardNumber.value.length == 16 && expiredDate.value.length == 4 && password.value.length == 4
-    }
-
-    fun setSelectedBank(bankType: BankType) {
-        _selectedBank.value = bankType
+    fun setSelectedBank(cardCompanyType: CardCompanyType) {
+        _selectedCardCompany.value = cardCompanyType
     }
 
     fun addCard(
         cardNumber: String,
         expiredDate: String,
         ownerName: String,
         password: String,
-        bankType: BankType,
+        cardCompanyType: CardCompanyType?,
     ) {
+        if (cardCompanyType == null) {
+            return
+        }
+
         paymentRepsoitory.addCard(
             Card(
                 cardNumber = cardNumber,
                 expiredDate = expiredDate,
                 ownerName = ownerName,
                 password = password,
-                bankType = bankType
+                cardCompanyType = cardCompanyType
             )
         )
     }
+
+    fun getCardById(cardId: String) {
+        modifyCard = paymentRepsoitory.getCardById(cardId)
+
+        modifyCard?.let { card ->
+            _cardNumber.value = card.cardNumber
+            _expiredDate.value = card.expiredDate
+            _ownerName.value = card.ownerName
+            _password.value = card.password
+            _selectedCardCompany.value = card.cardCompanyType
+        }
+    }
+
+    fun updateCard(
+        cardId: String,
+        cardNumber: String,
+        expiredDate: String,
+        ownerName: String,
+        password: String,
+        cardCompanyType: CardCompanyType?,
+    ): Boolean {
path: app/src/main/java/nextstep/payments/ui/viewmodel/NewCardViewModel.kt
user: login: sjjeong
body: 넵! UDF라는 것에 대해 알아보시는 것도 좋겠네요
https://developer.android.com/develop/ui/compose/architecture#udf
created_at: 2025-03-14T14:42:07Z
author_association: NONE
diff_hunk: @@ -100,57 +110,71 @@ fun NewCardScreen(
     }
 
     LaunchedEffect(Unit) {
-        viewModel.updateCardState.collect { isUpdatable ->
+        viewModel.updateCardState.collectLatest { isUpdatable ->
             if (!isUpdatable) {
                 coroutineScope.launch {
                     snackbarHostState.showSnackbar(snackbarMessage)
                 }
 
-                return@collect
+                return@collectLatest
             }
 
             navigateToCardList()
         }
     }
 
+    LaunchedEffect(Unit) {
+        viewModel.saveState.collectLatest { state ->
path: app/src/main/java/nextstep/payments/ui/screen/NewCardScreen.kt
user: login: sjjeong
body: ViewModel에 있는 saveState를 collect했는데, 여기서 ViewModel의 함수를 호출하는게 이상하네요.
saveState에 따라서 처리해야할 것들을 전부 ViewModel에서 처리하고 View 관련 처리만 해야하는게 맞지 않을까요??
created_at: 2025-03-14T14:54:50Z
author_association: NONE
diff_hunk: @@ -80,65 +82,126 @@ fun NewCardScreen(
 
     // 스낵바 상태 저장
     val snackbarHostState = remember { SnackbarHostState() }
+    val coroutineScope = rememberCoroutineScope()
+
+    val snackbarMessage = if (cardId == null) {
+        stringResource(R.string.validate_snack_bar_message)
+    } else {
+        stringResource(R.string.validate_modify_snack_bar_message)
+    }
 
-    // 저장 가능 여부 유효성 체크
-    viewModel.setIsSaveEnabled()
+    val appBarTitle = if (cardId == null) {
+        stringResource(R.string.card_add_app_bar_title)
+    } else {
+        stringResource(R.string.card_update_app_bar_title)
+    }
+
+    LaunchedEffect(Unit) {
+        if (cardId != null) {
+            viewModel.fetchCardById(cardId)
+            isBottomSheetVisible = false
+        }
+    }
 
-    LaunchedEffect(selectedBank) {
-        if (selectedBank == BankType.NOT_SELECTED) {
-            isBottomSheetVisible = true
+    LaunchedEffect(isBottomSheetVisible) {
+        if (isBottomSheetVisible) {
             sheetState.show()
         }
+    }
 
-        if (selectedBank != BankType.NOT_SELECTED) {
-            isBottomSheetVisible = false
-            sheetState.hide()
+    LaunchedEffect(Unit) {
+        viewModel.updateCardState.collectLatest { isUpdatable ->
+            if (!isUpdatable) {
+                coroutineScope.launch {
+                    snackbarHostState.showSnackbar(snackbarMessage)
+                }
+
+                return@collectLatest
+            }
+
+            navigateToCardList()
+        }
path: app/src/main/java/nextstep/payments/ui/screen/NewCardScreen.kt
user: login: sjjeong
body: isUpdatable에 따라서 스낵바를 보여줄지 네비게이트할지 하는 로직이 View에 있는게 어색하네요.
관련 로직은 ViewModel에서 처리하고 View는 단순하게 스낵바를 보여줄지, naviagte를 할지 처리만 하면 되지 않을까요?
created_at: 2025-03-14T14:56:43Z
author_association: NONE
diff_hunk: @@ -46,30 +62,74 @@ class NewCardViewModel(
         _password.value = password
     }
 
-    fun setIsSaveEnabled() {
-        _isSaveEnabled.value =
-            cardNumber.value.length == 16 && expiredDate.value.length == 4 && password.value.length == 4
-    }
-
-    fun setSelectedBank(bankType: BankType) {
-        _selectedBank.value = bankType
+    fun setSelectedBank(cardCompanyType: CardCompanyType) {
+        _selectedCardCompany.value = cardCompanyType
     }
 
     fun addCard(
         cardNumber: String,
         expiredDate: String,
         ownerName: String,
         password: String,
-        bankType: BankType,
+        cardCompanyType: CardCompanyType?,
     ) {
+        if (cardCompanyType == null) {
+            return
+        }
+
         paymentRepsoitory.addCard(
             Card(
                 cardNumber = cardNumber,
                 expiredDate = expiredDate,
                 ownerName = ownerName,
                 password = password,
-                bankType = bankType
+                cardCompanyType = cardCompanyType
             )
         )
     }
+
+    fun getCardById(cardId: String) {
path: app/src/main/java/nextstep/payments/ui/viewmodel/NewCardViewModel.kt
user: login: mogunjoa
body: 아하.. 그렇군요..! 다양한 코드를 참고하도록 노력해봐야겠습니다 ㅠ0ㅠ
created_at: 2025-03-15T08:04:54Z
author_association: NONE
diff_hunk: @@ -46,30 +62,74 @@ class NewCardViewModel(
         _password.value = password
     }
 
-    fun setIsSaveEnabled() {
-        _isSaveEnabled.value =
-            cardNumber.value.length == 16 && expiredDate.value.length == 4 && password.value.length == 4
-    }
-
-    fun setSelectedBank(bankType: BankType) {
-        _selectedBank.value = bankType
+    fun setSelectedBank(cardCompanyType: CardCompanyType) {
+        _selectedCardCompany.value = cardCompanyType
     }
 
     fun addCard(
         cardNumber: String,
         expiredDate: String,
         ownerName: String,
         password: String,
-        bankType: BankType,
+        cardCompanyType: CardCompanyType?,
     ) {
+        if (cardCompanyType == null) {
+            return
+        }
+
         paymentRepsoitory.addCard(
             Card(
                 cardNumber = cardNumber,
                 expiredDate = expiredDate,
                 ownerName = ownerName,
                 password = password,
-                bankType = bankType
+                cardCompanyType = cardCompanyType
             )
         )
     }
+
+    fun getCardById(cardId: String) {
+        modifyCard = paymentRepsoitory.getCardById(cardId)
+
+        modifyCard?.let { card ->
+            _cardNumber.value = card.cardNumber
+            _expiredDate.value = card.expiredDate
+            _ownerName.value = card.ownerName
+            _password.value = card.password
+            _selectedCardCompany.value = card.cardCompanyType
+        }
+    }
+
+    fun updateCard(
+        cardId: String,
+        cardNumber: String,
+        expiredDate: String,
+        ownerName: String,
+        password: String,
+        cardCompanyType: CardCompanyType?,
+    ): Boolean {
path: app/src/main/java/nextstep/payments/ui/viewmodel/NewCardViewModel.kt
user: login: mogunjoa
body: 아하 개념은 알고 있었는데 제대로 활용하지 못하고 있었던 것 같습니다. 다시 한번 공부해봐야겠군요!
created_at: 2025-03-15T08:06:52Z
author_association: NONE
diff_hunk: @@ -100,57 +110,71 @@ fun NewCardScreen(
     }
 
     LaunchedEffect(Unit) {
-        viewModel.updateCardState.collect { isUpdatable ->
+        viewModel.updateCardState.collectLatest { isUpdatable ->
             if (!isUpdatable) {
                 coroutineScope.launch {
                     snackbarHostState.showSnackbar(snackbarMessage)
                 }
 
-                return@collect
+                return@collectLatest
             }
 
             navigateToCardList()
         }
     }
 
+    LaunchedEffect(Unit) {
+        viewModel.saveState.collectLatest { state ->
path: app/src/main/java/nextstep/payments/ui/screen/NewCardScreen.kt
user: login: mogunjoa
body: 확실히 ViewModel 내에서 충분히 로직처리를 할 수 있을 것 같습니다! 로직 구성할 때 좀 더 깊이 생각하면서 진행해야겠네요 ㅠㅜ 피드백 감사합니다!
created_at: 2025-03-15T08:21:57Z
author_association: NONE
diff_hunk: @@ -80,65 +82,126 @@ fun NewCardScreen(
 
     // 스낵바 상태 저장
     val snackbarHostState = remember { SnackbarHostState() }
+    val coroutineScope = rememberCoroutineScope()
+
+    val snackbarMessage = if (cardId == null) {
+        stringResource(R.string.validate_snack_bar_message)
+    } else {
+        stringResource(R.string.validate_modify_snack_bar_message)
+    }
 
-    // 저장 가능 여부 유효성 체크
-    viewModel.setIsSaveEnabled()
+    val appBarTitle = if (cardId == null) {
+        stringResource(R.string.card_add_app_bar_title)
+    } else {
+        stringResource(R.string.card_update_app_bar_title)
+    }
+
+    LaunchedEffect(Unit) {
+        if (cardId != null) {
+            viewModel.fetchCardById(cardId)
+            isBottomSheetVisible = false
+        }
+    }
 
-    LaunchedEffect(selectedBank) {
-        if (selectedBank == BankType.NOT_SELECTED) {
-            isBottomSheetVisible = true
+    LaunchedEffect(isBottomSheetVisible) {
+        if (isBottomSheetVisible) {
             sheetState.show()
         }
+    }
 
-        if (selectedBank != BankType.NOT_SELECTED) {
-            isBottomSheetVisible = false
-            sheetState.hide()
+    LaunchedEffect(Unit) {
+        viewModel.updateCardState.collectLatest { isUpdatable ->
+            if (!isUpdatable) {
+                coroutineScope.launch {
+                    snackbarHostState.showSnackbar(snackbarMessage)
+                }
+
+                return@collectLatest
+            }
+
+            navigateToCardList()
+        }
path: app/src/main/java/nextstep/payments/ui/screen/NewCardScreen.kt
user: login: mogunjoa
body: 맞는 말씀입니다. 위 로직 역시 위에서 말씀하신바와 같이 UI 내에서는 필요없고 saveState를 활용하여 충분히 같은 로직을 구성할 수 있습니다. 바로 수정해보겠습니다!
created_at: 2025-03-15T08:30:03Z
author_association: NONE

PR comments:
user: login: sjjeong
created_at: 2025-03-13T02:00:01Z
body: 전체적으로 bank라는게 아직 남아있는데 같이 변경하면 좋을 것 같습니다.
author_association: NONE
user: login: mogunjoa
created_at: 2025-03-14T07:58:40Z
body: 안녕하십니까 석준님!
늘 좋은 리뷰 감사드립니다!
덕분에 많은걸 알게되고 있는 것 같습니다ㅎㅎ

말씀해주신 개선사항 반영해서 PR 요청드립니다.
궁금했던 점은 위에 댓글로 달아두었습니다! 아래 좌표도 남겨드립니다!
https://github.com/next-step/android-payments/pull/110#discussion_r1994902349
https://github.com/next-step/android-payments/pull/110#discussion_r1994903640

추가로 한가지 더 궁금했던 점이 있는데 제가 구현하면서 SaveState라는 저장 상태를 나타내는 sealed class 를 활용해서 검증 로직을 구성해보았습니다. 혹시 이렇게 로직을 구성해도 괜찮은 방법일가요?
[9097dc4](https://github.com/next-step/android-payments/pull/110/commits/9097dc4510a0562dcf5417277aecd998ddd1c79e)
author_association: NONE
user: login: mogunjoa
created_at: 2025-03-15T08:36:48Z
body: 석준님 안녕하세요!
피드백 주신 부분 개선해보았습니다!
이제 UI와 ViewModel 각각의 영역에서 구현해줘야할 로직에 대해 감이 잡혀가는 것 같습니다!
덕분에 정말 많은 부분을 배워갑니다!
좋은 리뷰 감사합니다!
author_association: NONE