ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,70 @@
+package store.model.repository;
+
+import store.Message.ErrorMessage;
+import store.config.DataInitializer;
+import store.config.ProductsData;
+import store.model.domain.Product;
+import store.constant.ProductType;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class ProductRepository {
+    public static final String DEFAULT_PRODUCT_FILE_PATH = "src/main/resources/products.md";
+
+    private final Map<ProductType, Map<String, Product>> info = new LinkedHashMap<>();
+    private final Map<ProductType, Map<String, Integer>> stock = new HashMap<>();
+
+    public ProductRepository(String filePath) {
+        DataInitializer initializer = DataInitializer.getInstance();
+        try {
+            ProductsData productsData = initializer.loadProducts(filePath);
+            this.info.putAll(productsData.info());
+            this.stock.putAll(productsData.stock());
+        } catch (IOException e) {
+            throw new RuntimeException(ErrorMessage.FILE_ERROR.getMessage(filePath));
+        }
+    }
+
+    public ProductRepository() {
+        this(DEFAULT_PRODUCT_FILE_PATH);
+    }
+
+    public List<Product> findAllInfo() {
+        return info.values().stream()
+                .flatMap(innerMap -> innerMap.values().stream())
+                .toList();
+    }
+
+    public Product findInfo(String name, ProductType productType) {
+        Map<String, Product> typeInfo = info.get(productType);
+        return typeInfo.getOrDefault(name, null);
+    }
+
+    public int findStock(String name, ProductType productType) {
+        Map<String, Integer> typeStock = stock.get(productType);
+        return typeStock.getOrDefault(name, 0);
+    }
+
+    public boolean reduceStock(String name, ProductType productType, int quantity) {
+        Map<String, Integer> typeStocks = stock.get(productType);
+        checkStock(name, typeStocks);
+
+        int currentStock = typeStocks.get(name);
+        if (currentStock < quantity) {
+            return false;
+        }
+
+        typeStocks.put(name, currentStock - quantity);
+        return true;
+    }
+
+    private static void checkStock(String name, Map<String, Integer> stock) {
+        if (stock == null || !stock.containsKey(name)) {
+            throw new IllegalStateException(ErrorMessage.NON_EXISTENT_PRODUCT.getMessage());
+        }
+    }
+}
\ No newline at end of file

리뷰: productInfo 였지만 ProductRepository 내부라서 info로 변경했습니다. 혹시 description과 같은 명명이 더 좋았을까요?
@@ -0,0 +1,70 @@
+package store.model.repository;
+
+import store.Message.ErrorMessage;
+import store.config.DataInitializer;
+import store.config.ProductsData;
+import store.model.domain.Product;
+import store.constant.ProductType;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class ProductRepository {
+    public static final String DEFAULT_PRODUCT_FILE_PATH = "src/main/resources/products.md";
+
+    private final Map<ProductType, Map<String, Product>> info = new LinkedHashMap<>();
+    private final Map<ProductType, Map<String, Integer>> stock = new HashMap<>();
+
+    public ProductRepository(String filePath) {
+        DataInitializer initializer = DataInitializer.getInstance();
+        try {
+            ProductsData productsData = initializer.loadProducts(filePath);
+            this.info.putAll(productsData.info());
+            this.stock.putAll(productsData.stock());
+        } catch (IOException e) {
+            throw new RuntimeException(ErrorMessage.FILE_ERROR.getMessage(filePath));
+        }
+    }
+
+    public ProductRepository() {
+        this(DEFAULT_PRODUCT_FILE_PATH);
+    }
+
+    public List<Product> findAllInfo() {
+        return info.values().stream()
+                .flatMap(innerMap -> innerMap.values().stream())
+                .toList();
+    }
+
+    public Product findInfo(String name, ProductType productType) {
+        Map<String, Product> typeInfo = info.get(productType);
+        return typeInfo.getOrDefault(name, null);
+    }
+
+    public int findStock(String name, ProductType productType) {
+        Map<String, Integer> typeStock = stock.get(productType);
+        return typeStock.getOrDefault(name, 0);
+    }
+
+    public boolean reduceStock(String name, ProductType productType, int quantity) {
+        Map<String, Integer> typeStocks = stock.get(productType);
+        checkStock(name, typeStocks);
+
+        int currentStock = typeStocks.get(name);
+        if (currentStock < quantity) {
+            return false;
+        }
+
+        typeStocks.put(name, currentStock - quantity);
+        return true;
+    }
+
+    private static void checkStock(String name, Map<String, Integer> stock) {
+        if (stock == null || !stock.containsKey(name)) {
+            throw new IllegalStateException(ErrorMessage.NON_EXISTENT_PRODUCT.getMessage());
+        }
+    }
+}
\ No newline at end of file

리뷰: 감사합니다. optional로 반환했다면 호출하는 곳에서 더 깔끔하게 사용할 수 있겠네요!
@@ -1,7 +1,27 @@
 package store;
 
+import camp.nextstep.edu.missionutils.Console;
+import store.controller.StoreController;
+import store.model.repository.ProductRepository;
+import store.model.repository.PromotionRepository;
+import store.model.service.ReceiptCalculationService;
+import store.model.service.StoreService;
+import store.view.InputView;
+import store.view.OutputView;
+
 public class Application {
     public static void main(String[] args) {
-        // TODO: 프로그램 구현
+        ProductRepository productRepository = new ProductRepository();
+        PromotionRepository promotionRepository = new PromotionRepository();
+
+        StoreService storeService = new StoreService(productRepository, promotionRepository);
+        ReceiptCalculationService receiptCalculationService = new ReceiptCalculationService();
+
+        InputView inputView = new InputView();
+        OutputView outputView = new OutputView();
+        StoreController storeController = new StoreController(inputView, outputView, storeService, receiptCalculationService);
+
+        storeController.run();
+        Console.close();
     }
 }

리뷰: Repository 계층을 사용하면 얻을 수 있는 장점이 무엇인지 궁금합니다!
@@ -0,0 +1,24 @@
+package store.Message;
+
+public enum ErrorMessage {
+
+    INVALID_FORMAT("올바르지 않은 형식으로 입력했습니다."),
+    NON_EXISTENT_PRODUCT("존재하지 않는 상품입니다."),
+    EXCEEDS_STOCK("재고 수량을 초과하여 구매할 수 없습니다."),
+    GENERIC_ERROR("잘못된 입력입니다."),
+    FILE_ERROR("잘못된 데이터 파일입니다:");
+
+    private final String message;
+
+    ErrorMessage(String message) {
+        this.message = message;
+    }
+
+    public String getMessage() {
+        return String.format("[ERROR] %s 다시 입력해 주세요.", message);
+    }
+
+    public String getMessage(String detail) {
+        return String.format("[ERROR] %s : %s", message, detail);
+    }
+}
\ No newline at end of file

리뷰: 메시지 포멧 형식에서 값을 삽입하는 방법이 신기합니다. 이 부분도 상수로 정의해 적용해 보는 건 어떨까요!?
@@ -0,0 +1,14 @@
+package store.constant;
+
+public class ReceiptLabels {
+    public static final String HEADER = "\n==============W 편의점================";
+    public static final String GIFT_HEADER = "=============증\t정===============";
+    public static final String FOOTER = "====================================";
+    public static final String TOTAL_LABEL = "총구매액";
+    public static final String EVENT_DISCOUNT_LABEL = "행사할인";
+    public static final String MEMBERSHIP_DISCOUNT_LABEL = "멤버십할인";
+    public static final String FINAL_AMOUNT_LABEL = "내실돈";
+    public static final String PRODUCT_NAME = "상품명";
+    public static final String COUNT = "수량";
+    public static final String PRICE = "금액";
+}
\ No newline at end of file

리뷰: 저도 이 부분이 가장 큰 고민인데, Enum을 활용할 수 있지만 매번 .getMessage() 메서드 사용으로 로직이 복잡해지는 느낌이 있습니다. 하지만, 그래도 타입을 보장하기 때문에 Enum 사용을 권장한다고 합니다. 이에 대한 의견을 듣고 싶습니다.
@@ -0,0 +1,27 @@
+package store.util;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Stream;
+
+public class StringParser {
+
+    private StringParser() {
+    }
+
+    public static Map<String, Integer> parseToMap(String input) {
+        Map<String, Integer> result = new HashMap<>();
+
+        Stream.of(input.split(","))
+                .forEach(item -> {
+                    item = item.replaceAll("[\\[\\]]", "");
+                    String[] nameAndQuantity = item.split("-");
+                    if (nameAndQuantity.length != 2) {
+                        throw new IllegalArgumentException("상품명과 수량을 정확히 입력해 주세요.");
+                    }
+                    result.put(nameAndQuantity[0], Integer.valueOf(nameAndQuantity[1]));
+                });
+
+        return result;
+    }
+}
\ No newline at end of file

리뷰: 2번째 값에 숫자가 아닌 문자가 올 경우 에러 처리를 못하지 않나용!?
@@ -0,0 +1,34 @@
+package store.model.domain;
+
+import store.constant.ProductType;
+
+public class Product {
+    private final String name;
+    private final int price;
+    private final String promotion;
+
+    public Product(String name, int price, String promotion) {
+        this.name = name;
+        this.price = price;
+        this.promotion = promotion;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public int getPrice() {
+        return price;
+    }
+
+    public String getPromotion() {
+        return promotion;
+    }
+
+    public ProductType isPromotion() {
+        if (promotion.equals("null")) {
+            return ProductType.REGULAR;
+        }
+        return ProductType.PROMOTION;
+    }
+}

리뷰: "null" 도 상수로 분리해 프로모션이 없을 때 라는 명칭을 가진 이름을 명시하는 게 좋을 거 같습니다.
@@ -0,0 +1,47 @@
+package store.model.service;
+
+import store.model.domain.ShoppingCartCheck;
+import store.model.ReceiptTotals;
+import store.constant.Discounts;
+
+import java.util.List;
+
+public class ReceiptCalculationService {
+
+    public ReceiptTotals calculateTotals(List<ShoppingCartCheck> shoppingCartChecks, boolean isMembership) {
+        ReceiptTotals totals = new ReceiptTotals();
+
+        for (ShoppingCartCheck dto : shoppingCartChecks) {
+            int price = dto.getProductPrice() * dto.getRequestCount();
+            totals.totalCount += dto.getRequestCount();
+            totals.totalPrice += price;
+            totals.regularPrice += dto.getRegularCount() * dto.getProductPrice();
+        }
+
+        totals.giftPrice = calculateGiftDiscount(shoppingCartChecks);
+        totals.membershipPrice = calculateMembershipDiscount(totals.regularPrice, isMembership);
+
+        return totals;
+    }
+
+    private int calculateGiftDiscount(List<ShoppingCartCheck> shoppingCartChecks) {
+        int giftPrice = 0;
+        for (ShoppingCartCheck dto : shoppingCartChecks) {
+            if (dto.getFreeCount() > 0 && dto.isActivePromotion()) {
+                giftPrice -= dto.getProductPrice() * dto.getFreeCount();
+            }
+        }
+        return giftPrice;
+    }
+
+    private int calculateMembershipDiscount(int regularPrice, boolean isMembership) {
+        if (isMembership) {
+            double discountPrice = regularPrice * -Discounts.MEMBERSHIP_DISCOUNT_RATE;
+            if (Math.abs(discountPrice) > Discounts.MAX_MEMBERSHIP_DISCOUNT) {
+                return -Discounts.MAX_MEMBERSHIP_DISCOUNT;
+            }
+            return (int) discountPrice;
+        }
+        return 0;
+    }
+}
\ No newline at end of file

리뷰: 이 부분도 메서드를 분리해 인덴트를 감소시킬 수 있을 거 같아요!
@@ -1,7 +1,27 @@
 package store;
 
+import camp.nextstep.edu.missionutils.Console;
+import store.controller.StoreController;
+import store.model.repository.ProductRepository;
+import store.model.repository.PromotionRepository;
+import store.model.service.ReceiptCalculationService;
+import store.model.service.StoreService;
+import store.view.InputView;
+import store.view.OutputView;
+
 public class Application {
     public static void main(String[] args) {
-        // TODO: 프로그램 구현
+        ProductRepository productRepository = new ProductRepository();
+        PromotionRepository promotionRepository = new PromotionRepository();
+
+        StoreService storeService = new StoreService(productRepository, promotionRepository);
+        ReceiptCalculationService receiptCalculationService = new ReceiptCalculationService();
+
+        InputView inputView = new InputView();
+        OutputView outputView = new OutputView();
+        StoreController storeController = new StoreController(inputView, outputView, storeService, receiptCalculationService);
+
+        storeController.run();
+        Console.close();
     }
 }

리뷰: 의존성 주입 받는 부분이 꽤 많아서 복잡도가 조금 높아 보이는데 이에 대해 어떻게 생각하시나요?
@@ -0,0 +1,24 @@
+package store.Message;
+
+public enum ErrorMessage {
+
+    INVALID_FORMAT("올바르지 않은 형식으로 입력했습니다."),
+    NON_EXISTENT_PRODUCT("존재하지 않는 상품입니다."),
+    EXCEEDS_STOCK("재고 수량을 초과하여 구매할 수 없습니다."),
+    GENERIC_ERROR("잘못된 입력입니다."),
+    FILE_ERROR("잘못된 데이터 파일입니다:");
+
+    private final String message;
+
+    ErrorMessage(String message) {
+        this.message = message;
+    }
+
+    public String getMessage() {
+        return String.format("[ERROR] %s 다시 입력해 주세요.", message);
+    }
+
+    public String getMessage(String detail) {
+        return String.format("[ERROR] %s : %s", message, detail);
+    }
+}
\ No newline at end of file

리뷰: [ERROR] 와 "다시 입력해 주세요." 도 상수 처리하는 것이 좋아 보입니다!
@@ -0,0 +1,8 @@
+package store.config;
+
+public record ProductData(String name,
+                          int price,
+                          int quantity,
+                          String promotion) {
+}
+

리뷰: record 사용하신 점 인상 깊습니다 :) 저도 IntelliJ에서 record 사용을 추천해서 변환하려다가 부작용이 있을까봐 시도하지 않았거든요, 혹시 record를 사용하면 좋은 장점같은 것을 말씀해주실 수 있을까요?
@@ -0,0 +1,9 @@
+package store.constant;
+
+public class InputMessages {
+    public static final String PURCHASE_PROMPT_MESSAGE = "\n구매하실 상품명과 수량을 입력해 주세요. (예: [사이다-2],[감자칩-1])";
+    public static final String MEMBERSHIP_PROMPT_MESSAGE = "\n멤버십 할인을 받으시겠습니까? (Y/N)";
+    public static final String ADDITIONAL_PURCHASE_PROMPT_MESSAGE = "\n감사합니다. 구매하고 싶은 다른 상품이 있나요? (Y/N)";
+    public static final String ADDITIONAL_FREE_PROMPT_MESSAGE = "\n현재 %s은(는) 1개를 무료로 더 받을 수 있습니다. 추가하시겠습니까? (Y/N)%n";
+    public static final String REGULAR_PRICE_PROMPT_MESSAGE = "\n현재 %s %d개는 프로모션 할인이 적용되지 않습니다. 그래도 구매하시겠습니까? (Y/N)%n";
+}

리뷰: 어떤 것은 개행 문자가 \n 이고, 어떤 것은 %n 인것에 이유가 있나요? %n으로 통일했으면 좋겠다는 의견입니다 :)
@@ -0,0 +1,14 @@
+package store.constant;
+
+public class ReceiptLabels {
+    public static final String HEADER = "\n==============W 편의점================";
+    public static final String GIFT_HEADER = "=============증\t정===============";
+    public static final String FOOTER = "====================================";
+    public static final String TOTAL_LABEL = "총구매액";
+    public static final String EVENT_DISCOUNT_LABEL = "행사할인";
+    public static final String MEMBERSHIP_DISCOUNT_LABEL = "멤버십할인";
+    public static final String FINAL_AMOUNT_LABEL = "내실돈";
+    public static final String PRODUCT_NAME = "상품명";
+    public static final String COUNT = "수량";
+    public static final String PRICE = "금액";
+}
\ No newline at end of file

리뷰: 저도 매번 .getMessage() 를 사용하는 것이 약간 복잡하게 느껴지더라고요.. 이에 대한 해결책이 있다면 공유받고 싶습니다!
@@ -0,0 +1,18 @@
+package store.constant;
+
+public enum FileConfig {
+    FILE_HEADER(0),
+    PROMOTION_HEADER_SIZE(5),
+    PRODUCT_HEADER_SIZE(4),
+    ;
+
+    private final int value;
+
+    FileConfig(int value) {
+        this.value = value;
+    }
+
+    public int getValue() {
+        return value;
+    }
+}

리뷰: 파일 이름도 여기에 정의가 되면 좋을 것 같습니다.
@@ -0,0 +1,23 @@
+package store.controller;
+
+import java.util.List;
+import store.dto.request.ProductInputDto;
+import store.dto.request.PromotionTypeInputDto;
+import store.model.product.PromotionTypeManager;
+import store.model.product.ProductManager;
+
+public class ProductController {
+    private final FileInputHandler fileInputHandler;
+
+    public ProductController() {
+        this.fileInputHandler = new FileInputHandler();
+    }
+
+    public ProductManager initialize() {
+        List<PromotionTypeInputDto> promotionTypeInputDtos = fileInputHandler.getPromotions();
+        PromotionTypeManager promotionTypeManager = new PromotionTypeManager(promotionTypeInputDtos);
+
+        List<ProductInputDto> productInputDtos = fileInputHandler.getProducts();
+        return new ProductManager(promotionTypeManager, productInputDtos);
+    }
+}

리뷰: 해당 클래스 `ProductController` 는 컨트롤러보다 '서비스'에 가깝다고 봅니다.

initialize 메소드를 보면 파일 읽기 -> Entity 생성 -> Entity 반환 입니다. 
흐름을 보았을 때는 컨트롤러 개념과 살짝 거리가 있어보이는데, 컨트롤러로 구현하시신 이유가 있나요?
@@ -0,0 +1,7 @@
+package store.exception;
+
+public class ExceptionUtils {
+    public static void throwIllegalArgumentException(ExceptionMessage exceptionMessage) {
+        throw new IllegalArgumentException(exceptionMessage.getMessage());
+    }
+}

리뷰: 저도 이렇게 한적이 있는데요, 이렇게 하면 최초 에러 발생지가 `ExceptionUtils`가 되어버려서 발생한 예외를 추천하는데 있어 불필요한 예러 전파 단계가 추가 됩니다. 그래서 저는 커스텀 예외를 만들어서 처리하였습니다!
@@ -0,0 +1,4 @@
+package store.dto.response;
+
+public record PromotionBenefitResultDto(String productName, int promotionBenefitQuantity) {
+}

리뷰: requestDTO와 responseDTO 패키지를 나눠주신 게 가독성이 좋은 것 같습니다.👍
@@ -0,0 +1,64 @@
+package store.model.order;
+
+import java.util.Objects;
+import java.util.Optional;
+import store.model.product.Product;
+import store.model.product.PromotionType;
+
+public class OrderItem implements Comparable<OrderItem> {
+    private final Product product;
+    private int quantity;
+
+    public OrderItem(Product product, int quantity) {
+        this.product = product;
+        this.quantity = quantity;
+    }
+
+    @Override
+    public int compareTo(OrderItem o) {
+        if (this.product.getPromotionType() == null && o.product.getPromotionType() != null) {
+            return 1;
+        }
+        if (this.product.getPromotionType() != null && o.product.getPromotionType() == null) {
+            return -1;
+        }
+        return 0;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        OrderItem orderItem = (OrderItem) o;
+        return quantity == orderItem.quantity && Objects.equals(product, orderItem.product);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(product, quantity);
+    }
+
+    public int getQuantity() {
+        return quantity;
+    }
+
+    public Product getProduct() {
+        return product;
+    }
+
+    public void addQuantity(int quantity) {
+        this.quantity += quantity;
+    }
+
+    public Optional<PromotionType> getPromotionType() {
+        return product.getPromotionType();
+    }
+
+    public String getProductName() {
+        return product.getName();
+    }
+}

리뷰: 와 compareTo를 통해서 프로모션상품이 먼저 정렬되도록 하셨군요? 이런 방법도 있었네요. 저는 Promotion상품인지 아닌지 구분해서 처리하도록 했는데 훨씬 더 유연한 방법인 것 같습니다.
@@ -0,0 +1,56 @@
+package store.model.order;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import store.constant.StoreConfig;
+import store.dto.response.PromotionResultDto;
+
+public class Membership {
+    public static double calculateMembershipDiscount(Order order, List<PromotionResultDto> promotionResult) {
+        Map<String, List<OrderItem>> groupedOrderItems = groupOrderItemsByProductName(order);
+
+        long totalAmountWithoutPromotion = calculateTotalAmountWithoutPromotion(groupedOrderItems, promotionResult);
+        long totalPromotionAmount = calculateTotalPromotionAmount(order, promotionResult);
+
+        double discountRate = StoreConfig.BASIC_MEMBERSHIP.getValue() / 100.0;
+        double discountAmount = (totalAmountWithoutPromotion + totalPromotionAmount) * discountRate;
+
+        return Math.min(discountAmount, StoreConfig.BASIC_MEMBERSHIP_LIMIT.getValue());
+    }
+
+    private static Map<String, List<OrderItem>> groupOrderItemsByProductName(Order order) {
+        return order.getOrderItems().stream()
+                .collect(Collectors.groupingBy(OrderItem::getProductName));
+    }
+
+    private static long calculateTotalAmountWithoutPromotion(Map<String, List<OrderItem>> groupedOrderItems,
+                                                             List<PromotionResultDto> promotionResult) {
+        return groupedOrderItems.entrySet().stream()
+                .filter(entry -> !isPromotionProduct(entry.getKey(), promotionResult))
+                .flatMap(entry -> entry.getValue().stream())
+                .mapToLong(orderItem -> (long) orderItem.getQuantity() * orderItem.getProduct().getPrice())
+                .sum();
+    }
+
+    private static boolean isPromotionProduct(String productName, List<PromotionResultDto> promotionResult) {
+        return promotionResult.stream()
+                .anyMatch(promotion -> promotion.productName().equals(productName) && promotion.benefitQuantity() > 0);
+    }
+
+    private static int calculateTotalPromotionAmount(Order order, List<PromotionResultDto> promotionResult) {
+        return promotionResult.stream()
+                .filter(promotion -> promotion.remainingQuantity() > 0 && promotion.benefitQuantity() > 0)
+                .mapToInt(promotion -> calculateAmountForRemainingQuantity(order, promotion))
+                .sum();
+    }
+
+    private static int calculateAmountForRemainingQuantity(Order order, PromotionResultDto promotionResultDto) {
+        return (int) order.findOrderItemByProductName(promotionResultDto.productName()).stream()
+                .mapToLong(orderItem -> {
+                    int applicableQuantity = Math.min(orderItem.getQuantity(), promotionResultDto.remainingQuantity());
+                    return (long) applicableQuantity * orderItem.getProduct().getPrice();
+                })
+                .sum();
+    }
+}

리뷰: 이부분도 quantity와 price를 통해 orderItem이 직접 totalPrice를 계산하도록 하면 좋을 것 같습니다.
@@ -1 +1,68 @@
-# java-convenience-store-precourse
+# 편의점
+
+## 프로젝트 소개
+구매자의 할인 혜택과 재고 상황을 고려하여 최종 결제 금액을 계산하고 안내하는 결제 시스템
+- 사용자가 입력한 상품의 가격과 수량을 기반으로 최종 결제 금액을 계산한다.
+- 총구매액은 상품별 가격과 수량을 곱하여 계산하며, 프로모션 및 멤버십 할인 정책을 반영하여 최종 결제 금액을 산출한다.
+- 구매 내역과 산출한 금액 정보를 영수증으로 출력한다.
+- 영수증 출력 후 추가 구매를 진행할지 또는 종료할지를 선택할 수 있다.
+
+## 기능 구현 목록
+
+### 1. 입력 처리
+- [x] **상품 목록 및 행사 목록 파일 입력**
+  - 상품 목록과 행사 목록을 파일 입력을 통해 저장한다.
+- [x] **상품 및 수량 입력**
+  - 구매할 상품과 수량을 `[상품명-수량]` 형식으로 입력받는다.
+- [x] **무료 상품 추가 여부 입력**
+  - 프로모션 혜택을 통해 무료 상품을 받을 수 있는 경우, 상품 추가 여부를 입력받는다.
+- [x] **프로모션 재고 부족 시 일부 수량 정가 결제 여부 입력**
+  - 프로모션 재고가 부족할 경우, 프로모션 미적용 수량에 대한 일반 결제 여부를 입력받는다.
+- [x] **멤버십 할인 적용 여부 입력**
+  - 멤버십 할인 적용 여부를 입력받는다.
+- [x] **추가 구매 여부 입력**
+  - 추가 구매 여부를 입력받는다.
+
+### 2. 출력 처리
+- [x] **상품 목록 출력**
+  - 환영 메시지와 함께 현재 재고가 있는 상품 목록을 화면에 출력한다. 재고가 없는 상품은 "재고 없음"으로 표시한다.
+- [x] **프로모션 추가 상품 증정 안내 메시지 출력**
+  - 고객이 가져온 수량보다 더 많은 수량을 프로모션 혜택으로 받을 수 있는 경우, 추가 혜택 수량을 안내한다.
+- [x] **프로모션 재고 부족 시 정가 결제 안내 메시지 출력**
+  - 프로모션 재고가 부족하여 일부 수량을 프로모션 혜택 없이 결제해야 하는 경우, 일반 결제 여부를 묻는 메시지를 출력한다.
+- [x] **멤버십 할인 적용 여부 안내 메시지 출력**
+  - 멤버십 할인을 적용할지 여부를 묻는 메시지를 출력한다.
+- [x] **영수증 출력**
+  - 구매 내역, 증정 상품, 총구매액, 행사할인, 멤버십 할인 금액, 최종 결제 금액을 보기 좋게 정렬하여 영수증으로 출력한다.
+- [x] **추가 구매 여부 안내 메시지 출력**
+  - 추가 구매를 원하는지 여부를 묻는 메시지를 출력한다.
+
+### 3. 주문 정보 관리
+- [x] **주문 정보 저장**
+  - 입력된 값을 기반으로 주문 정보를 저장한다.
+- [x] **결제 가능 여부 확인**
+  - 각 상품의 재고 수량과 구매 수량을 비교하여 결제 가능 여부를 확인한다.
+- [x] **주문 정보 수정**
+  - 무료 상품 추가 및 프로모션 미적용 상품 차감의 경우 구매 수량을 수정한다.
+
+### 4. 결제 및 재고 관리
+- [x] **재고 업데이트**
+  - 구매한 수량만큼 재고를 차감하여 재고 상태를 업데이트한다.
+- [x] **결제 정보 계산 및 저장**
+  - 결제 금액 산출을 위한 각 항목별 판매량을 계산한다.
+    - 프로모션 상품, 증정품, 일반 상품 정보와 멤버십 적용 여부를 저장한다.
+- [x] **영수증 생성**
+  - 결제 정보를 토대로 영수증 출력에 필요한 값을 계산한다.
+    - 구매 상품 내역, 증정 상품 내역, 총 수량 및 금액, 프로모션 할인 금액, 멤버십 할인 금액, 최종 결제 금액
+
+### 5. 예외 처리
+- [x] **잘못된 입력값 예외 처리**
+  - 사용자가 잘못된 값을 입력할 경우 `IllegalArgumentException`를 발생시키고, `[ERROR]`로 시작하는 에러 메시지를 출력한 후 해당 부분부터 입력을 다시 받는다.
+    - [x] 구매할 상품과 수량 형식이 올바르지 않은 경우
+      - `[ERROR] 올바르지 않은 형식으로 입력했습니다. 다시 입력해 주세요.`
+    - [x] 존재하지 않는 상품을 입력한 경우
+      - `[ERROR] 존재하지 않는 상품입니다. 다시 입력해 주세요.`
+    - [x] 구매 수량이 재고 수량을 초과한 경우
+      - `[ERROR] 재고 수량을 초과하여 구매할 수 없습니다. 다시 입력해 주세요.`
+    - [x] 기타 잘못된 입력의 경우
+      - `[ERROR] 잘못된 입력입니다. 다시 입력해 주세요.`
\ No newline at end of file

리뷰: 요구사항을 깔끔하게 잘 나누어 둔 것 같습니다