ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,75 @@
+package vendingmachine.controller;
+
+import vendingmachine.model.domain.Coins;
+import vendingmachine.model.domain.Products;
+import vendingmachine.model.domain.VendingMachine;
+import vendingmachine.model.service.VendingMachineService;
+import vendingmachine.view.InputView;
+import vendingmachine.view.OutputView;
+
+public class VendingMachineController {
+    private final VendingMachineService vendingMachineService;
+
+    public VendingMachineController(VendingMachineService vendingMachineService) {
+        this.vendingMachineService = vendingMachineService;
+    }
+
+    public void start() {
+        OutputView.promptInputMachineHave();
+        int money = InputView.vendingMachineHave();
+
+        VendingMachine vendingMachine = vendingMachineService.getVendingMachine(money);
+        OutputView.promptMachineHave(vendingMachine);
+
+        getProducts(vendingMachine);
+        int inputMoney = getInputMoney();
+
+        inputMoney = getInputMoney(vendingMachine, inputMoney);
+        getReturnChange(vendingMachine, inputMoney);
+    }
+
+    private static void getProducts(VendingMachine vendingMachine) {
+        OutputView.promptInputProducts();
+        Products products = InputView.buyProducts();
+        vendingMachine.addProducts(products);
+    }
+
+    private static int getInputMoney() {
+        OutputView.promptInputMoney();
+        return InputView.inputMoney();
+    }
+
+    private static int getInputMoney(VendingMachine vendingMachine, int inputMoney) {
+        while (inputMoney > 0) {
+            int minPrice = vendingMachine.findMinimumProductPrice();
+            if (inputMoney < minPrice) {
+                break;
+            }
+            if (vendingMachine.areProductsSoldOut()) {
+                break;
+            }
+
+            OutputView.promptBuying(inputMoney);
+            String buyProductName = InputView.buyProduct();
+
+            inputMoney = calculateInputMoney(vendingMachine, inputMoney, buyProductName);
+        }
+        return inputMoney;
+    }
+
+    private static void getReturnChange(VendingMachine vendingMachine, int inputMoney) {
+        Coins change = vendingMachine.returnChange(inputMoney);
+        OutputView.promptReturnChange(change);
+    }
+
+    private static int calculateInputMoney(VendingMachine vendingMachine, int inputMoney, String buyProductName) {
+        try {
+            int productPrice = vendingMachine.findProductPrice(buyProductName);
+            inputMoney -= productPrice;
+            vendingMachine.purchaseProduct(buyProductName);
+        } catch (IllegalArgumentException e) {
+            OutputView.promptErrorMessage(e.getMessage());
+        }
+        return inputMoney;
+    }
+}

리뷰: 구현하면서 Controller 로직이 복잡하다는 점이 아쉬웠는데, 이 점은 계속해서 개선해봐야겠어요!
@@ -0,0 +1,75 @@
+package vendingmachine.controller;
+
+import vendingmachine.model.domain.Coins;
+import vendingmachine.model.domain.Products;
+import vendingmachine.model.domain.VendingMachine;
+import vendingmachine.model.service.VendingMachineService;
+import vendingmachine.view.InputView;
+import vendingmachine.view.OutputView;
+
+public class VendingMachineController {
+    private final VendingMachineService vendingMachineService;
+
+    public VendingMachineController(VendingMachineService vendingMachineService) {
+        this.vendingMachineService = vendingMachineService;
+    }
+
+    public void start() {
+        OutputView.promptInputMachineHave();
+        int money = InputView.vendingMachineHave();
+
+        VendingMachine vendingMachine = vendingMachineService.getVendingMachine(money);
+        OutputView.promptMachineHave(vendingMachine);
+
+        getProducts(vendingMachine);
+        int inputMoney = getInputMoney();
+
+        inputMoney = getInputMoney(vendingMachine, inputMoney);
+        getReturnChange(vendingMachine, inputMoney);
+    }
+
+    private static void getProducts(VendingMachine vendingMachine) {
+        OutputView.promptInputProducts();
+        Products products = InputView.buyProducts();
+        vendingMachine.addProducts(products);
+    }
+
+    private static int getInputMoney() {
+        OutputView.promptInputMoney();
+        return InputView.inputMoney();
+    }
+
+    private static int getInputMoney(VendingMachine vendingMachine, int inputMoney) {
+        while (inputMoney > 0) {
+            int minPrice = vendingMachine.findMinimumProductPrice();
+            if (inputMoney < minPrice) {
+                break;
+            }
+            if (vendingMachine.areProductsSoldOut()) {
+                break;
+            }
+
+            OutputView.promptBuying(inputMoney);
+            String buyProductName = InputView.buyProduct();
+
+            inputMoney = calculateInputMoney(vendingMachine, inputMoney, buyProductName);
+        }
+        return inputMoney;
+    }
+
+    private static void getReturnChange(VendingMachine vendingMachine, int inputMoney) {
+        Coins change = vendingMachine.returnChange(inputMoney);
+        OutputView.promptReturnChange(change);
+    }
+
+    private static int calculateInputMoney(VendingMachine vendingMachine, int inputMoney, String buyProductName) {
+        try {
+            int productPrice = vendingMachine.findProductPrice(buyProductName);
+            inputMoney -= productPrice;
+            vendingMachine.purchaseProduct(buyProductName);
+        } catch (IllegalArgumentException e) {
+            OutputView.promptErrorMessage(e.getMessage());
+        }
+        return inputMoney;
+    }
+}

리뷰: 말씀하신 부분 다시 살펴보니 도메인에 위임하는게 더 좋겠네요!
@@ -0,0 +1,59 @@
+package vendingmachine.model.domain;
+
+import static vendingmachine.constant.ErrorMessage.INVALID_PRODUCT_NAME;
+import static vendingmachine.constant.ErrorMessage.INVALID_PRODUCT_PRICE;
+import static vendingmachine.constant.ErrorMessage.INVALID_PRODUCT_QUANTITY;
+import static vendingmachine.constant.ErrorMessage.PRODUCT_SOLD_OUT;
+import static vendingmachine.constant.NumberConstant.*;
+
+public class Product {
+    private final String productName;
+    private final int price;
+    private int quantity;
+
+    public Product(String productName, int price, int quantity) {
+        validateProductName(productName);
+        validatePrice(price);
+        validateQuantity(quantity);
+        this.productName = productName;
+        this.price = price;
+        this.quantity = quantity;
+    }
+
+    private void validateProductName(String productName) {
+        if (productName == null || productName.isEmpty()) {
+            throw new IllegalArgumentException(INVALID_PRODUCT_NAME.getMessage());
+        }
+    }
+
+    private void validatePrice(int price) {
+        if (price < MINIMUM_MONEY_PRICE || price % DIVIDE_MONEY_UNIT != 0) {
+            throw new IllegalArgumentException(INVALID_PRODUCT_PRICE.getMessage());
+        }
+    }
+
+    private void validateQuantity(int quantity) {
+        if (quantity < 0) {
+            throw new IllegalArgumentException(INVALID_PRODUCT_QUANTITY.getMessage());
+        }
+    }
+
+    public int getPrice() {
+        return price;
+    }
+
+    public boolean isNameSame(String inputProductName) {
+        return this.productName.equals(inputProductName);
+    }
+
+    public boolean isQuantityZero() {
+        return this.quantity == 0;
+    }
+
+    public void decreaseQuantity() {
+        if (quantity <= 0) {
+            throw new IllegalArgumentException(PRODUCT_SOLD_OUT.getMessage());
+        }
+        this.quantity -= 1;
+    }
+}

리뷰: 말씀해주신 부분처럼 재고 수량 조절을 위해서 final 키워드를 생략했습니다!
@@ -0,0 +1,43 @@
+package vendingmachine.model.domain;
+
+import static vendingmachine.constant.ErrorMessage.PRODUCT_IS_NOT_EXIST;
+import static vendingmachine.constant.ErrorMessage.NOT_ENOUGH_MONEY;
+
+import java.util.List;
+
+public class Products {
+    private final List<Product> products;
+
+    public Products(List<Product> products) {
+        this.products = products;
+    }
+
+    public int findPriceByProductName(String productName) {
+        return products.stream()
+            .filter(product -> product.isNameSame(productName))
+            .findAny()
+            .orElseThrow(() -> new IllegalArgumentException(PRODUCT_IS_NOT_EXIST.getMessage()))
+            .getPrice();
+    }
+
+    public int findMinimumPrice() {
+        return products.stream()
+            .filter(product -> !product.isQuantityZero())
+            .mapToInt(Product::getPrice)
+            .min()
+            .orElseThrow(() -> new IllegalArgumentException(NOT_ENOUGH_MONEY.getMessage()));
+    }
+
+    public boolean areSoldOut() {
+        return products.stream().allMatch(Product::isQuantityZero);
+    }
+
+    public void decreaseProductQuantity(String productName) {
+        Product product = products.stream()
+            .filter(name -> name.isNameSame(productName))
+            .findAny()
+            .orElseThrow(() -> new IllegalArgumentException(PRODUCT_IS_NOT_EXIST.getMessage()));
+
+        product.decreaseQuantity();
+    }
+}

리뷰: 감사합니다!!:)
@@ -0,0 +1,73 @@
+package vendingmachine.util;
+
+import static vendingmachine.constant.ErrorMessage.MONEY_SHOULD_BE_NUMBER;
+import static vendingmachine.constant.ErrorMessage.MONEY_SHOULD_BE_POSITIVE;
+import static vendingmachine.constant.ErrorMessage.INVALID_PRODUCT_FORMAT;
+import static vendingmachine.constant.NumberConstant.*;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import vendingmachine.model.domain.Product;
+import vendingmachine.model.domain.Products;
+
+public class Parser {
+    private static final String SEMICOLON = ";";
+    private static final String LEFT_BRACKET = "[";
+    private static final String RIGHT_BRACKET = "]";
+    private static final String COMMA = ",";
+
+    public static int parseInteger(String input) {
+        try {
+            int money = Integer.parseInt(input);
+            validateNegative(money);
+            return money;
+        } catch (NumberFormatException e) {
+            throw new IllegalArgumentException(MONEY_SHOULD_BE_NUMBER.getMessage());
+        }
+    }
+
+    private static void validateNegative(int money) {
+        if(money < 0) {
+            throw new IllegalArgumentException(MONEY_SHOULD_BE_POSITIVE.getMessage());
+        }
+    }
+
+    public static Products parseProducts(String inputProducts) {
+        List<Product> products = parseProductList(inputProducts);
+        return new Products(products);
+    }
+
+    private static List<Product> parseProductList(String inputProducts) {
+        String[] splitProducts = inputProducts.split(SEMICOLON);
+        List<Product> products = new ArrayList<>();
+
+        for (String splitProduct : splitProducts) {
+            validateProductFormat(splitProduct);
+            Product product = parseProduct(splitProduct);
+            products.add(product);
+        }
+        return products;
+    }
+
+    private static void validateProductFormat(String productInfo) {
+        if (!productInfo.startsWith(LEFT_BRACKET) || !productInfo.endsWith(RIGHT_BRACKET)) {
+            throw new IllegalArgumentException(INVALID_PRODUCT_FORMAT.getMessage());
+        }
+    }
+
+    private static Product parseProduct(String productInfo) {
+        String content = productInfo.substring(1, productInfo.length() - 1);
+        String[] splitProductInfo = content.split(COMMA);
+
+        if (splitProductInfo.length != PRODUCTS_LENGTH) {
+            throw new IllegalArgumentException(INVALID_PRODUCT_FORMAT.getMessage());
+        }
+
+        String name = splitProductInfo[0].trim();
+        int price = Integer.parseInt(splitProductInfo[1].trim());
+        int quantity = Integer.parseInt(splitProductInfo[2].trim());
+
+        return new Product(name, price, quantity);
+    }
+}

리뷰: 마음이 급하다보니 더 쉬운 방법을 택했던 것 같아요! 정규표현식을 사용해도 좋을 것 같네요:)
@@ -0,0 +1,36 @@
+package vendingmachine.model.domain;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+class ProductTest {
+    @Test
+    @DisplayName("상품 가격이 100원부터 시작하지 않는 경우 예외가 발생한다.")
+    void 상품_가격이_100원부터_시작하지_않는_경우_예외_발생() {
+        assertThatThrownBy(() -> new Product("콜라", 90, 1))
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    @DisplayName("상품 가격이 10원으로 나누어 떨어지지지 않는 경우 예외가 발생한다.")
+    void 상품_가격이_10원으로_나누어_떨어지지_않는_경우_예외_발생() {
+        assertThatThrownBy(() -> new Product("콜라", 9, 1))
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    @DisplayName("상품명이 비어있는 경 예외가 발생한다.")
+    void 상품명이_비어있는_경우_예외_발생() {
+        assertThatThrownBy(() -> new Product("", 1000, 1))
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    @DisplayName("상품 수량이 0 미만인 경우 예외가 발생한다.")
+    void 상품_수량이_0_미만인_경우_예외_발생() {
+        assertThatThrownBy(() -> new Product("콜라", 1000, -1))
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+}

리뷰: 넵 추가로 테스트 코드 리팩토링까지는 생각 못했네요,,😅
@@ -0,0 +1,25 @@
+package store.constants;
+
+public enum ErrorMessage {
+
+    PRODUCT_BUY_FORM_ERROR("올바르지 않은 형식으로 입력했습니다. 다시 입력해 주세요."),
+    NO_EXSIST_PRODUCT_ERROR("존재하지 않는 상품 입니다. 다시 입력해 주세요"),
+    NOT_ENOUGH_QUANTITY_ERROR("재고 수량을 초과하여 구매할 수 없습니다. 다시 입력해 주세요."),
+    NOT_POSITIVE_INPUT_ERROR("구입할 수량은 1 이상이어야 합니다. 다시 입력해 주세요."),
+    ETC_INPUT_ERROR("잘못된 입력입니다. 다시 입력해주세요."),
+    DUPLICATE_PRODUCT_INPUT_ERROR("같은 상품은 한 번에 여러개 구입합니다. 다시 입력해 주세요."),
+    PRODUCT_ERROR("잘못된 재고입니다."),
+    SYSTEM_ERROR("실행 오류 입니다.");
+
+    private final String prefix = "[ERROR] ";
+    private final String postfix = "\n";
+    private final String message;
+
+    ErrorMessage(String message) {
+        this.message = prefix + message + postfix;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+}

리뷰: prefix와 postfix는 공통된 부분이어서 static으로 만드셔도 좋을 거 같아요!
@@ -0,0 +1,19 @@
+package store.constants;
+
+public enum StringConstants {
+
+    ZERO_STRING("0"),
+    NO_PROMOTION("null"),
+    YES("Y"),
+    NO("N");
+
+    private final String string;
+
+    StringConstants(String string) {
+        this.string = string;
+    }
+
+    public String getString() {
+        return string;
+    }
+}

리뷰: enum으로 잘 묶으셨네요!
YES와 NO를 제외한 다른 상수는 관련성이 없어 보이는데, 따로 enum으로 묶으신 이유가 궁금합니다!
@@ -0,0 +1,82 @@
+package store.domain;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import store.constants.ErrorMessage;
+import store.constants.StringConstants;
+
+public class Products {
+
+    private List<String> productNames = new ArrayList<>();
+    private final Map<String, Product> promotionProducts = new LinkedHashMap<>();
+    private final Map<String, Product> regularProducts = new LinkedHashMap<>();
+
+    public Products(List<Product> allProducts) {
+        for (Product product : allProducts) {
+            addProduct(product);
+        }
+    }
+
+    public List<String> getProductNames() {
+        return productNames;
+    }
+
+    public Map<String, Product> getPromotionProducts() {
+        return promotionProducts;
+    }
+
+    public Map<String, Product> getRegularProducts() {
+        return regularProducts;
+    }
+
+    public void setProductNames(List<String> productNames) {
+        this.productNames = productNames;
+    }
+
+    private void addProduct(Product product) {
+        if (product.hasPromotion()) {
+            promotionProducts.put(product.getName(), product);
+        }
+        if (!product.hasPromotion()) {
+            regularProducts.put(product.getName(), product);
+        }
+    }
+
+    public void addNoRegularProducts() {
+        for (String productName : productNames) {
+            if (!regularProducts.containsKey(productName)) {
+                Product product = promotionProducts.get(productName);
+                regularProducts.put(productName, new Product(productName, String.valueOf(product.getPrice()),
+                        StringConstants.ZERO_STRING.getString(), StringConstants.NO_PROMOTION.getString()));
+            }
+        }
+    }
+
+    public boolean hasProduct(String productName) {
+        if (!promotionProducts.containsKey(productName) && !regularProducts.containsKey(productName)) {
+            throw new IllegalArgumentException(ErrorMessage.NO_EXSIST_PRODUCT_ERROR.getMessage());
+        }
+        return promotionProducts.containsKey(productName) || regularProducts.containsKey(productName);
+    }
+
+    public void updatePromotionProductQuantity(String name, int quantity) {
+        Product product = promotionProducts.get(name);
+        product.updateQuantity(quantity);
+    }
+
+    public void updateRegularProductQuantity(String name, int quantity) {
+        Product product = regularProducts.get(name);
+        product.updateQuantity(quantity);
+    }
+
+    public boolean hasSufficientStock(String productName, int quantity) {
+        int totalQuantity = regularProducts.get(productName).getQuantity();
+        if (promotionProducts.containsKey(productName)) {
+            totalQuantity += promotionProducts.get(productName).getQuantity();
+        }
+
+        return totalQuantity >= quantity;
+    }
+}

리뷰: 프로모션 상품과 일반 상품을 잘 나누셨네요!
`hasPromotion`이 중복되어서 아래와 같이 하는 건 어떨까요?
```java
if (product.hasPromotion()) {
    promotionProducts.put(product.getName(), product);
    return;
}

regularProducts.put(product.getName(), product);
```
@@ -0,0 +1,83 @@
+package store.domain;
+
+import java.util.List;
+import store.constants.StringConstants;
+
+public class TotalPrice {
+
+    private static final int MAX_MEMBERSHIP_SALE = 8000;
+    private int totalCount = 0;
+    private int totalPrice = 0;
+    private int promotionPrice = 0;
+    private double memberShipPrice = 0;
+
+    public TotalPrice(List<Purchase> purchases, Products products) {
+        getTotalPrice(purchases, products);
+    }
+
+    public int getTotalCount() {
+        return totalCount;
+    }
+
+    public int getPromotionPrice() {
+        return promotionPrice;
+    }
+
+    public int getTotalPrice() {
+        return totalPrice;
+    }
+
+    public void setPromotionPrice(int promotionPrice) {
+        this.promotionPrice = promotionPrice;
+    }
+
+    public double getMemberShipPrice() {
+        return memberShipPrice;
+    }
+
+    public void setMemberShipPrice(double memberShipPrice) {
+        this.memberShipPrice = memberShipPrice;
+    }
+
+    private void getTotalPrice(List<Purchase> purchases, Products products) {
+        for (Purchase purchase : purchases) {
+            int price = products.getRegularProducts().get(purchase.getName()).getPrice();
+            this.totalPrice += purchase.getQuantity() * price;
+            this.totalCount += purchase.getQuantity();
+        }
+    }
+
+    public int getPromotionSalePrice(List<Purchase> purchases, Promotions promotions, Products products) {
+        int promotionPrice = 0;
+        for (Purchase purchase : purchases) {
+            if (purchase.getPromotion().equals(StringConstants.NO_PROMOTION.getString())) {
+                continue;
+            }
+            Promotion promotion = promotions.getPromotions().get(purchase.getPromotion());
+            int price = products.getRegularProducts().get(purchase.getName()).getPrice();
+            promotionPrice += price * promotion.freeCount(purchase.getQuantity());
+        }
+        return promotionPrice;
+    }
+
+    public double getMembershipSalePrice(List<Purchase> purchases, Products products) {
+        int afterPromotionSalePrice = totalPrice - getPurchasePromotionPrice(purchases, products);
+        double membershipPrice = afterPromotionSalePrice * 0.3;
+
+        if (membershipPrice > MAX_MEMBERSHIP_SALE) {
+            return MAX_MEMBERSHIP_SALE;
+        }
+        return membershipPrice;
+    }
+
+    private static int getPurchasePromotionPrice(List<Purchase> purchases, Products products) {
+        int promotionPrice = 0;
+        for (Purchase purchase : purchases) {
+            if (!purchase.getPromotion().equals(StringConstants.NO_PROMOTION.getString())) {
+                Product product = products.getRegularProducts().get(purchase.getName());
+                promotionPrice += purchase.getQuantity() * product.getPrice();
+            }
+        }
+        return promotionPrice;
+    }
+}

리뷰: `0.3`도 상수화하시면, 의미가 더 명확해질 거 같아요!
@@ -0,0 +1,21 @@
+package store.service;
+
+import store.constants.StringConstants;
+import store.domain.Cart;
+import store.domain.Products;
+import store.domain.Purchase;
+
+public class CartService {
+
+    public Products productsReduce(Products products, Cart cart) {
+        for (Purchase purchase : cart.getItems()) {
+            if (purchase.getPromotion().equals(StringConstants.NO_PROMOTION.getString())) {
+                products.getRegularProducts().get(purchase.getName()).updateQuantity(purchase.getQuantity());
+            }
+            if (!purchase.getPromotion().equals(StringConstants.NO_PROMOTION.getString())) {
+                products.getPromotionProducts().get(purchase.getName()).updateQuantity(purchase.getQuantity());
+            }
+        }
+        return products;
+    }
+}

리뷰: 이 부분이 계속 반복되어서, `purchase.hasPromotion`으로 만드시면 더 가독성이 좋아질 거 같아요!
@@ -0,0 +1,30 @@
+package store.utils;
+
+import java.io.BufferedReader;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class FileRead {
+
+    public static List<String> readFile(String fileName) throws IOException {
+        BufferedReader br = new BufferedReader(new FileReader(fileName));
+        List<String> contents = readFileContents(br);
+
+        br.close();
+        contents.removeFirst();
+        return contents;
+    }
+
+    private static List<String> readFileContents(BufferedReader br) throws IOException {
+        List<String> contents = new ArrayList<>();
+        while (true) {
+            String line = br.readLine();
+            if (line == null) break;
+            contents.add(line);
+        }
+        return contents;
+    }
+}

리뷰: 파일 읽어오는 부분을 잘 작성하셨네요!
BufferedReader의 `lines()`메서드도 활용해보시면 좋을 거 같아요!
@@ -0,0 +1,62 @@
+package store.domain;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import store.constants.ErrorMessage;
+
+class CartTest {
+
+    private Cart cart;
+    private List<Purchase> purchases;
+
+    @BeforeEach
+    void setUp() {
+        purchases = new ArrayList<>();
+        purchases.add(new Purchase("콜라", 3, "탄산2+1"));
+        purchases.add(new Purchase("사이다", 5, "null"));
+        cart = new Cart(purchases);
+    }
+
+    @Test
+    void 장바구니_생성_테스트() {
+        // given
+        List<Purchase> expectedItems = purchases;
+
+        // then
+        assertThat(cart.getItems().size()).isEqualTo(expectedItems.size());
+        assertThat(cart.getItems()).containsAll(expectedItems);
+    }
+
+    @Test
+    void 중복_아이템_추가_예외_테스트() {
+        // given
+        purchases.add(new Purchase("콜라", 2, "탄산2+1"));
+
+        // when & then
+        assertThatThrownBy(() -> new Cart(purchases))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessage(ErrorMessage.DUPLICATE_PRODUCT_INPUT_ERROR.getMessage());
+    }
+
+    @Test
+    void 아이템_병합_테스트() {
+        // given
+        List<Purchase> additionalPurchases = new ArrayList<>(cart.getItems());;
+        additionalPurchases.add(new Purchase("콜라", 2, "탄산2+1"));
+        additionalPurchases.add(new Purchase("물", 4, "null"));
+
+        // when
+        Map<String, Purchase> mergedItems = cart.mergeItems(additionalPurchases);
+
+        // then
+        assertThat(mergedItems.get("콜라").getQuantity()).isEqualTo(5);  // 기존 3 + 추가 2
+        assertThat(mergedItems.containsKey("물")).isTrue();
+        assertThat(mergedItems.get("물").getQuantity()).isEqualTo(4);
+    }
+}
\ No newline at end of file

리뷰: 꼼꼼한 테스트가 인상적이네요!