ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,64 @@
+package store.io.input.impl;
+
+import camp.nextstep.edu.missionutils.Console;
+import store.common.ConsoleMessages;
+import store.common.ErrorMessages;
+import store.io.input.StoreInput;
+
+
+public class InputConsole implements StoreInput {
+
+    @Override
+    public String getPurchaseItemsInput() {
+        System.out.print(ConsoleMessages.LINE_SEPARATOR);
+        System.out.println(ConsoleMessages.PURCHASE_ITEMS_PROMPT);
+        return Console.readLine();
+    }
+
+    @Override
+    public boolean askForAdditionalPromo(String itemName, int additionalCount) {
+        System.out.print(ConsoleMessages.LINE_SEPARATOR);
+        System.out.printf(ConsoleMessages.ADDITIONAL_PROMO_PROMPT + ConsoleMessages.LINE_SEPARATOR, itemName,
+                additionalCount);
+        return getYesNoResponse();
+    }
+
+    @Override
+    public boolean askForFullPricePurchase(String itemName, int shortageCount) {
+        System.out.print(ConsoleMessages.LINE_SEPARATOR);
+        System.out.printf(ConsoleMessages.FULL_PRICE_PURCHASE_PROMPT + ConsoleMessages.LINE_SEPARATOR, itemName,
+                shortageCount);
+        return getYesNoResponse();
+    }
+
+    @Override
+    public boolean askForMembershipDiscount() {
+        System.out.print(ConsoleMessages.LINE_SEPARATOR);
+        System.out.println(ConsoleMessages.MEMBERSHIP_DISCOUNT_PROMPT);
+        return getYesNoResponse();
+    }
+
+    @Override
+    public boolean askForAdditionalPurchase() {
+        System.out.print(ConsoleMessages.LINE_SEPARATOR);
+        System.out.println(ConsoleMessages.ADDITIONAL_PURCHASE_PROMPT);
+        return getYesNoResponse();
+    }
+
+    private boolean getYesNoResponse() {
+        while (true) {
+            try {
+                String input = Console.readLine();
+                if ("Y".equals(input)) {
+                    return true;
+                }
+                if ("N".equals(input)) {
+                    return false;
+                }
+                throw new IllegalArgumentException(ErrorMessages.INVALID_INPUT_MESSAGE);
+            } catch (IllegalArgumentException e) {
+                System.out.println(ErrorMessages.ERROR_MESSAGE + e.getMessage());
+            }
+        }
+    }
+}

리뷰: 좋은 리뷰 감사해요 :)
@@ -0,0 +1,12 @@
+package store.common;
+
+public class ConsoleMessages {
+    public static final String WELCOME_MESSAGE = "안녕하세요. W편의점입니다.";
+    public static final String PRODUCT_LIST_HEADER = "현재 보유하고 있는 상품입니다.";
+    public static final String PURCHASE_ITEMS_PROMPT = "구매하실 상품명과 수량을 입력해 주세요. (예: [사이다-2],[감자칩-1])";
+    public static final String ADDITIONAL_PROMO_PROMPT = "현재 %s은(는) %d개를 무료로 더 받을 수 있습니다. 추가하시겠습니까? (Y/N)";
+    public static final String FULL_PRICE_PURCHASE_PROMPT = "현재 %s %d개는 프로모션 할인이 적용되지 않습니다. 그래도 구매하시겠습니까? (Y/N)";
+    public static final String MEMBERSHIP_DISCOUNT_PROMPT = "멤버십 할인을 받으시겠습니까? (Y/N)";
+    public static final String ADDITIONAL_PURCHASE_PROMPT = "감사합니다. 구매하고 싶은 다른 상품이 있나요? (Y/N)";
+    public static final String LINE_SEPARATOR = System.lineSeparator();
+}

리뷰: 저도 공통 피드백에서 상수화에 대해 올라와서 처음에는 모두 상수화를 진행하였습니다. 하지만 의문이 들어 하드 코딩된 내용이 어떤 내용인지 한번에 파악하기 힘든 경우에만 상수화를 하도록 저만의 기준을 잡았습니다!
@@ -0,0 +1,26 @@
+package store.config;
+
+import store.controller.ConvenienceStoreController;
+import store.io.input.StoreInput;
+import store.io.input.impl.InputConsole;
+import store.io.output.StoreOutput;
+import store.io.output.impl.OutputConsole;
+import store.service.ConvenienceStoreService;
+
+public class ConvenienceStoreConfig {
+    public StoreInput storeInput() {
+        return new InputConsole();
+    }
+
+    public StoreOutput storeOutput() {
+        return new OutputConsole();
+    }
+
+    public ConvenienceStoreService convenienceStoreService() {
+        return new ConvenienceStoreService();
+    }
+
+    public ConvenienceStoreController convenienceStoreController() {
+        return new ConvenienceStoreController(storeInput(), storeOutput(), convenienceStoreService());
+    }
+}

리뷰: 저도 받았던 지적인데 현재 Config에서 컨트롤러를 생성을 하면 매번 input, output,service 모두 새로운 객체를 생성하게됩니다! 일부로 의도하신건지는 모르겠지만 저는 추후에 싱글톤 패턴으로 리팩토링할 생각입니다!ㅎㅎ
@@ -0,0 +1,73 @@
+package store.domain;
+
+import camp.nextstep.edu.missionutils.DateTimes;
+import store.common.ErrorMessages;
+
+public class Product {
+    private static final String NO_PROMOTION_DESCRIPTION = "";
+
+    private final String name;
+    private final Money price;
+    private int stock;
+    private final Promotion promotion;
+
+    public Product(String name, int price, int stock, Promotion promotion) {
+        this.name = name;
+        this.price = new Money(price);
+        this.stock = stock;
+        this.promotion = promotion;
+    }
+
+    public Money calculatePrice(int quantity) {
+        return price.multiply(quantity);
+    }
+
+    public boolean isPromotionValid() {
+        return promotion != null && promotion.isValid(DateTimes.now().toLocalDate());
+    }
+
+    public void reduceRegularStock(int quantity) {
+        if (quantity > stock) {
+            throw new IllegalStateException(ErrorMessages.EXCEED_STOCK);
+        }
+        stock -= quantity;
+    }
+
+    public void reducePromotionStock(int promoQuantity) {
+        if (promoQuantity > stock) {
+            throw new IllegalStateException(ErrorMessages.EXCEED_STOCK);
+        }
+        stock -= promoQuantity;
+    }
+
+    public String getPromotionDescription() {
+        if (promotion != null) {
+            return promotion.getName();
+        }
+        return NO_PROMOTION_DESCRIPTION;
+    }
+
+    public void addStock(int additionalStock) {
+        this.stock += additionalStock;
+    }
+
+    public Money calculateTotalPrice(int quantity) {
+        return price.multiply(quantity);
+    }
+
+    public Money getPrice() {
+        return price;
+    }
+
+    public int getStock() {
+        return stock;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public Promotion getPromotion() {
+        return promotion;
+    }
+}

리뷰: 프로모션을 그대로 반환하는거보다 프로모션이 있는지만 확인하는 메소드를 구현하는건 어떨까요??
@@ -0,0 +1,17 @@
+package store.io.output;
+
+import java.util.List;
+import store.domain.Cart;
+import store.domain.Membership;
+import store.domain.Product;
+import store.domain.Receipt;
+
+public interface StoreOutput {
+    void printProductList(List<Product> products);
+
+    void printReceipt(Receipt receipt, Cart cart, Membership membership);
+
+    void printError(String message);
+
+    void close();
+}

리뷰: 인터페이스 돌입 이유가 궁금합니다!
@@ -0,0 +1,64 @@
+package store.io.input.impl;
+
+import camp.nextstep.edu.missionutils.Console;
+import store.common.ConsoleMessages;
+import store.common.ErrorMessages;
+import store.io.input.StoreInput;
+
+
+public class InputConsole implements StoreInput {
+
+    @Override
+    public String getPurchaseItemsInput() {
+        System.out.print(ConsoleMessages.LINE_SEPARATOR);
+        System.out.println(ConsoleMessages.PURCHASE_ITEMS_PROMPT);
+        return Console.readLine();
+    }
+
+    @Override
+    public boolean askForAdditionalPromo(String itemName, int additionalCount) {
+        System.out.print(ConsoleMessages.LINE_SEPARATOR);
+        System.out.printf(ConsoleMessages.ADDITIONAL_PROMO_PROMPT + ConsoleMessages.LINE_SEPARATOR, itemName,
+                additionalCount);
+        return getYesNoResponse();
+    }
+
+    @Override
+    public boolean askForFullPricePurchase(String itemName, int shortageCount) {
+        System.out.print(ConsoleMessages.LINE_SEPARATOR);
+        System.out.printf(ConsoleMessages.FULL_PRICE_PURCHASE_PROMPT + ConsoleMessages.LINE_SEPARATOR, itemName,
+                shortageCount);
+        return getYesNoResponse();
+    }
+
+    @Override
+    public boolean askForMembershipDiscount() {
+        System.out.print(ConsoleMessages.LINE_SEPARATOR);
+        System.out.println(ConsoleMessages.MEMBERSHIP_DISCOUNT_PROMPT);
+        return getYesNoResponse();
+    }
+
+    @Override
+    public boolean askForAdditionalPurchase() {
+        System.out.print(ConsoleMessages.LINE_SEPARATOR);
+        System.out.println(ConsoleMessages.ADDITIONAL_PURCHASE_PROMPT);
+        return getYesNoResponse();
+    }
+
+    private boolean getYesNoResponse() {
+        while (true) {
+            try {
+                String input = Console.readLine();
+                if ("Y".equals(input)) {
+                    return true;
+                }
+                if ("N".equals(input)) {
+                    return false;
+                }
+                throw new IllegalArgumentException(ErrorMessages.INVALID_INPUT_MESSAGE);
+            } catch (IllegalArgumentException e) {
+                System.out.println(ErrorMessages.ERROR_MESSAGE + e.getMessage());
+            }
+        }
+    }
+}

리뷰: 4주차 프로그래밍 요구사항 중 view를 구현하라는 내용이 있었던 것으로 기억합니다!
꼭 view라는 클래스 이름을 사용해야하는건 아니겠지만 InputConsole로 네이밍하신 이유가 궁금합니다!
@@ -0,0 +1,48 @@
+package store.domain;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class Cart {
+    private final List<CartItem> items;
+
+    public Cart() {
+        this.items = new ArrayList<>();
+    }
+
+    public void addItem(CartItem item) {
+        this.items.add(item);
+    }
+
+    public void replaceItem(int index, CartItem newItem) {
+        this.items.set(index, newItem);
+    }
+
+    public Money getTotalPrice() {
+        Money totalPrice = new Money(0);
+        for (CartItem item : items) {
+            totalPrice = totalPrice.add(item.calculateTotalPrice());
+        }
+        return totalPrice;
+    }
+
+    public int getTotalPromotionDiscount() {
+        int totalDiscount = 0;
+        for (CartItem item : items) {
+            totalDiscount += item.calculatePromotionDiscount();
+        }
+        return totalDiscount;
+    }
+
+    public int getTotalNonPromoAmount() {
+        int total = 0;
+        for (CartItem item : items) {
+            total += item.calculateTotalIfNoPromotion();
+        }
+        return total;
+    }
+
+    public List<CartItem> getItems() {
+        return List.copyOf(items);
+    }
+}

리뷰: stream을 사용하시면 어떨까요?!

```suggestion
        return items.stream()
            .mapToInt(CartItem::calculatePromotionDiscount)
            .sum();
```
@@ -0,0 +1,66 @@
+package store.domain;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class Receipt {
+    private static final int NO_QUANTITY = 0;
+
+    private final List<CartItem> purchasedItems;
+    private final List<CartItem> freeItems;
+    private final Money totalPrice;
+
+    public Receipt(Cart cart) {
+        this.purchasedItems = cart.getItems();
+        this.freeItems = calculateFreeItems(cart);
+        this.totalPrice = cart.getTotalPrice();
+    }
+
+    private List<CartItem> calculateFreeItems(Cart cart) {
+        List<CartItem> items = cart.getItems();
+        return getFreeItemsFromCart(items);
+    }
+
+    private List<CartItem> getFreeItemsFromCart(List<CartItem> items) {
+        List<CartItem> freeItems = new ArrayList<>();
+        for (CartItem item : items) {
+            addFreeItems(freeItems, item);
+        }
+        return freeItems;
+    }
+
+    private void addFreeItems(List<CartItem> freeItems, CartItem item) {
+        int freeQuantity = item.getFreeQuantity();
+        if (freeQuantity > NO_QUANTITY) {
+            freeItems.add(item);
+        }
+    }
+
+    public int getTotalQuantity() {
+        return purchasedItems.size();
+    }
+
+    public int getTotalPrice() {
+        return totalPrice.getAmount();
+    }
+
+    public int getPromotionDiscount(Cart cart) {
+        return cart.getTotalPromotionDiscount();
+    }
+
+    public int getMembershipDiscount(Cart cart, Membership membership) {
+        return membership.calculateDiscount(cart.getTotalNonPromoAmount());
+    }
+
+    public int getFinalPrice(Cart cart, Membership membership) {
+        return totalPrice.getAmount() - getPromotionDiscount(cart) - getMembershipDiscount(cart, membership);
+    }
+
+    public List<CartItem> getPurchasedItems() {
+        return purchasedItems;
+    }
+
+    public List<CartItem> getFreeItems() {
+        return freeItems;
+    }
+}

리뷰: 이 부분도 stream 사용하시면 가독성 측면에서 더 좋을 것 같아요!

```suggestion
        return items.stream()
            .filter(item -> isFreeItem(item))
            .collect(Collectors.toList());
```
@@ -0,0 +1,90 @@
+package store.controller;
+
+import java.util.List;
+import store.domain.Cart;
+import store.domain.Membership;
+import store.domain.ParsedItem;
+import store.domain.Receipt;
+import store.io.input.StoreInput;
+import store.io.output.StoreOutput;
+import store.service.ConvenienceStoreService;
+
+public class ConvenienceStoreController {
+    private final StoreInput storeInput;
+    private final StoreOutput storeOutput;
+    private final ConvenienceStoreService convenienceStoreService;
+
+    public ConvenienceStoreController(StoreInput storeInput, StoreOutput storeOutput,
+                                      ConvenienceStoreService convenienceStoreService) {
+        this.storeInput = storeInput;
+        this.storeOutput = storeOutput;
+        this.convenienceStoreService = convenienceStoreService;
+    }
+
+    public void start() {
+        boolean continueShopping;
+        try {
+            do {
+                executeShoppingCycle();
+                continueShopping = getValidAdditionalPurchaseResponse();
+            } while (continueShopping);
+        } finally {
+            storeOutput.close();
+        }
+    }
+
+    private void executeShoppingCycle() {
+        convenienceStoreService.printInventoryProductList(storeOutput);
+
+        List<ParsedItem> parsedItems = getValidParsedItems();
+        Cart cart = convenienceStoreService.createCart(parsedItems);
+
+        convenienceStoreService.applyPromotionToCartItems(cart, storeInput);
+
+        Membership membership = getValidMembershipResponse();
+
+        Receipt receipt = convenienceStoreService.createReceipt(cart);
+        storeOutput.printReceipt(receipt, cart, membership);
+
+        updateInventory(cart);
+    }
+
+    private List<ParsedItem> getValidParsedItems() {
+        while (true) {
+            try {
+                String input = storeInput.getPurchaseItemsInput();
+                return convenienceStoreService.parseItems(input);
+            } catch (IllegalArgumentException | IllegalStateException e) {
+                storeOutput.printError(e.getMessage());
+            }
+        }
+    }
+
+    private Membership getValidMembershipResponse() {
+        while (true) {
+            try {
+                return convenienceStoreService.determineMembership(storeInput);
+            } catch (IllegalArgumentException e) {
+                storeOutput.printError(e.getMessage());
+            }
+        }
+    }
+
+    private void updateInventory(Cart cart) {
+        try {
+            convenienceStoreService.updateInventory(cart);
+        } catch (IllegalStateException e) {
+            storeOutput.printError(e.getMessage());
+        }
+    }
+
+    private boolean getValidAdditionalPurchaseResponse() {
+        while (true) {
+            try {
+                return storeInput.askForAdditionalPurchase();
+            } catch (IllegalArgumentException e) {
+                storeOutput.printError(e.getMessage());
+            }
+        }
+    }
+}

리뷰: do-while문은 사용하지 않는게 좋다고 생각합니다
@@ -0,0 +1,31 @@
+package store.dto;
+
+import java.util.List;
+import java.util.Map;
+
+public class ReceiptInformation {
+
+    private Map<String, List> salesDatas;
+    private List<Integer> amountInformation;
+
+    public ReceiptInformation(Map<String, List> salesDatas, List<Integer> amountInformation) {
+        this.salesDatas = salesDatas;
+        this.amountInformation = amountInformation;
+    }
+
+    public Map<String, List> getSalesDatas() {
+        return salesDatas;
+    }
+
+    public void setSalesDatas(Map<String, List> salesDatas) {
+        this.salesDatas = salesDatas;
+    }
+
+    public List<Integer> getAmountInformation() {
+        return amountInformation;
+    }
+
+    public void setAmountInformation(List<Integer> amountInformation) {
+        this.amountInformation = amountInformation;
+    }
+}

리뷰: `XXXInformation`과 같은 네이밍보다는 어떤 정보가 담겨있는지 표현하시면 더 이해하기 쉬운 코드가 될 것 같습니다!
@@ -0,0 +1,22 @@
+package store.model.domain.product;
+
+import java.util.List;
+import java.util.Map;
+import store.model.domain.Promotion;
+
+public class ProductFactory {
+
+    private ProductFactory() {
+    }
+
+    public static Product createProductFrom(List<String> productData, Map<String, Promotion> promotions) {
+        String name = productData.get(0);
+        int price = Integer.parseInt(productData.get(1));
+        int quantity = Integer.parseInt(productData.get(2));
+        String promotion_name = productData.get(3);
+        if (promotion_name.equals("null")) {
+            return new NormalProduct(name, price, quantity);
+        }
+        return new PromotionProduct(name, price, quantity, promotions.get(promotion_name));
+    }
+}

리뷰: 물품을 일반 물품과 프로모션 물품으로 나누어 팩토리 패턴을 사용하신 부분이 좋아보이네요 👍
@@ -0,0 +1,28 @@
+package store.model.domain.product;
+
+public class ProductsDisplayData {
+
+    private String promotionProductData;
+    private String normalProductData;
+
+    public ProductsDisplayData(String promotionProductData, String normalProductData) {
+        this.promotionProductData = promotionProductData;
+        this.normalProductData = normalProductData;
+    }
+
+    public String getPromotionProductData() {
+        return promotionProductData;
+    }
+
+    public void setPromotionProductData(String promotionProductData) {
+        this.promotionProductData = promotionProductData;
+    }
+
+    public String getNormalProductData() {
+        return normalProductData;
+    }
+
+    public void setNormalProductData(String normalProductData) {
+        this.normalProductData = normalProductData;
+    }
+}

리뷰: 해당 부분은 OutputView에서 출력하기 위해 사용하시는 것 같은데 그게 맞다면 DTO로 분류하셔도 괜찮을 것 같아요!
@@ -0,0 +1,9 @@
+package store.constant;
+
+public class ConstantBox {
+    public static final String SEPARATOR = ",";
+    public static final String NO_QUANTITY = "0";
+    public static final String INPUT_SEPARATOR = "-";
+    public static final String CUSTOMER_RESPOND_Y = "Y";
+    public static final String CUSTOMER_RESPOND_N = "N";
+}

리뷰: 여러 대안이 있었을 것 같은데, 해당 클래스를 사용하신 이유가 궁금해요! (사용하는 클래스 내부 상수처리 라던지, enum 이라던지...)
@@ -0,0 +1,49 @@
+package store.dto;
+
+import store.model.domain.PurchaseResponseCode;
+
+public class PurchaseResponse {
+
+    private String name;
+    private PurchaseResponseCode purchaseResponseCode;
+    private int promotionCount;
+    private int restCount;
+
+    public PurchaseResponse(PurchaseResponseCode purchaseResponseCode, int promotionCount, int restCount) {
+        this.purchaseResponseCode = purchaseResponseCode;
+        this.promotionCount = promotionCount;
+        this.restCount = restCount;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public PurchaseResponseCode getPurchaseResponseCode() {
+        return purchaseResponseCode;
+    }
+
+    public void setPurchaseResponseCode(PurchaseResponseCode purchaseResponseCode) {
+        this.purchaseResponseCode = purchaseResponseCode;
+    }
+
+    public int getPromotionCount() {
+        return promotionCount;
+    }
+
+    public void setPromotionCount(int promotionCount) {
+        this.promotionCount = promotionCount;
+    }
+
+    public int getRestCount() {
+        return restCount;
+    }
+
+    public void setRestCount(int restCount) {
+        this.restCount = restCount;
+    }
+}

리뷰: 해당 메서드는 어떤 역할을 하는 지 좀 더 명확한 네이밍을 쓰면 좋을 것 같아요! (set... 네이밍보다는)
@@ -0,0 +1,9 @@
+package store.constant;
+
+public class ConstantBox {
+    public static final String SEPARATOR = ",";
+    public static final String NO_QUANTITY = "0";
+    public static final String INPUT_SEPARATOR = "-";
+    public static final String CUSTOMER_RESPOND_Y = "Y";
+    public static final String CUSTOMER_RESPOND_N = "N";
+}

리뷰: 여러 클래스에서 사용되는 동일한 단순 값들을 한 클래스에서 모아 관리하고자 상수 전용 클래스를 만들게 되었습니다!

예전에 에러 메세지들을 enum으로 관리 했었는데요! 단순 Strimg 메세지들만 들어있는데 추가적으로 getter를 사용해서 값을 가져오다 보니 가독성이 떨어지고 enum의 진정한 쓰임이라고 보기 힘들 것 같다는 리뷰를 접한 적이 있습니다.
그래서 단순 값들이 여러 클래스에서 사용될 때는 상수들을 모아 놓은 클래스를 통해 공유해서 사용하면 좋겠다는 생각이 들어 이렇게 관리하게 된 것 같습니다!
@@ -0,0 +1,28 @@
+package store.model.domain.product;
+
+public class ProductsDisplayData {
+
+    private String promotionProductData;
+    private String normalProductData;
+
+    public ProductsDisplayData(String promotionProductData, String normalProductData) {
+        this.promotionProductData = promotionProductData;
+        this.normalProductData = normalProductData;
+    }
+
+    public String getPromotionProductData() {
+        return promotionProductData;
+    }
+
+    public void setPromotionProductData(String promotionProductData) {
+        this.promotionProductData = promotionProductData;
+    }
+
+    public String getNormalProductData() {
+        return normalProductData;
+    }
+
+    public void setNormalProductData(String normalProductData) {
+        this.normalProductData = normalProductData;
+    }
+}

리뷰: 오 맞습니다! 이 부분도 마지막 제출이 급해서 빠트리게 되었네요,,
좋은 피드백 감사합니다!
@@ -0,0 +1,76 @@
+package store.model;
+
+import camp.nextstep.edu.missionutils.DateTimes;
+
+import java.text.NumberFormat;
+import java.time.LocalDateTime;
+
+public class Product {
+    private final String name;
+    private int price;
+    private int quantity;
+    private Promotion promotion;
+
+    public Product(String name, int price, int quantity, Promotion promotion) {
+        this.name = name;
+        this.price = price;
+        this.quantity = quantity;
+        this.promotion = promotion;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public int getQuantity() {
+        return quantity;
+    }
+
+    public int getPrice() {
+        return price;
+    }
+
+    public int addPromotions(int number) {
+        int buy = promotion.getBuy();
+        int get = promotion.getGet();
+        if (number % (buy + get) >= buy) {
+            return get - (number % (buy + get) - buy);
+        }
+        return 0;
+    }
+
+    public int applyPromotions(int number) {
+        int buy = promotion.getBuy();
+        int get = promotion.getGet();
+        return (number / (get + buy)) * get;
+    }
+
+    public boolean isPromotion() {
+        if (promotion == null) {
+            return false;
+        }
+        LocalDateTime time = DateTimes.now();
+        if (time.isBefore(promotion.getStartDate()) || time.isAfter(promotion.getEndDate())) {
+            return false;
+        }
+        return true;
+    }
+
+    public void setQuantity(int quantity) {
+        this.quantity = quantity;
+    }
+
+    public int getPromotionBuy() {
+        return promotion.getBuy();
+    }
+
+    @Override
+    public String toString() {
+        NumberFormat formatter = NumberFormat.getInstance();
+        StringBuilder result = new StringBuilder("- " + name + " " + formatter.format(price) + "원");
+        if (quantity > 0) result.append(" ").append(quantity).append("개");
+        if (quantity == 0) result.append(" 재고 없음");
+        if (promotion != null) result.append(" ").append(promotion.getName());
+        return result.toString();
+    }
+}

리뷰: 아래처럼 축약하면 어떨까요? 흠 너무 긴가요?!🤔
```suggestion
        return !time.isBefore(promotion.getStartDate()) && !time.isAfter(promotion.getEndDate());
```