ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,60 @@
+package store.model.promotion;
+
+import static store.common.constant.PromotionNotice.DEFAULT_NOTICE;
+import static store.common.constant.PromotionNotice.GET_FREE_M_NOTICE;
+import static store.common.constant.PromotionNotice.OUT_OF_STOCK_NOTICE;
+
+import store.common.dto.PromotionConditionResult;
+import store.common.dto.PromotionResult;
+import store.model.Product;
+
+public class BuyNGetMFreePromotion implements PromotionStrategy {
+
+    private final int purchaseQuantity;
+    private final int freeQuantity;
+
+    public BuyNGetMFreePromotion(int purchaseQuantity, int freeQuantity) {
+        this.purchaseQuantity = purchaseQuantity;
+        this.freeQuantity = freeQuantity;
+    }
+
+    @Override
+    public PromotionResult applyPromotion(Product product, int quantity) {
+        int totalFreeQuantity = getApplicableSetCount(quantity) * freeQuantity;
+        return PromotionResult.of(product.name(), product.price() * totalFreeQuantity, totalFreeQuantity);
+    }
+
+    @Override
+    public PromotionConditionResult checkCondition(Product product, int quantity) {
+        int setCountFromRequest = getApplicableSetCount(quantity);
+        int setCountFromStock = getApplicableSetCount(product.stock());
+        int applicableSetCount = Math.min(setCountFromRequest, setCountFromStock);
+        int stockUsed = applicableSetCount * getPromotionSetSize();
+        int remainingQuantity = quantity - stockUsed;
+
+        String message = getConditionMessage(product, product.stock() - stockUsed, remainingQuantity);
+        return PromotionConditionResult.from(applicableSetCount, stockUsed, remainingQuantity, message);
+    }
+
+    private String getConditionMessage(Product product, int remainingStock, int remainingQuantity) {
+        if (remainingQuantity == purchaseQuantity && remainingStock > purchaseQuantity) {
+            return GET_FREE_M_NOTICE.message(product.name(), freeQuantity);
+        }
+        if (remainingQuantity >= getPromotionSetSize()) {
+            return OUT_OF_STOCK_NOTICE.message(product.name(), remainingQuantity);
+        }
+        return DEFAULT_NOTICE.message();
+    }
+
+    private int getPromotionSetSize() {
+        return purchaseQuantity + freeQuantity;
+    }
+
+    private int getApplicableSetCount(int quantity) {
+        return quantity / getPromotionSetSize();
+    }
+
+    private int getRemainingQuantity(int quantity) {
+        return quantity % getPromotionSetSize();
+    }
+}

리뷰: PromotionStrategy 구현체는 BuyNGetMFreePromotion 뿐 인가요?
@@ -0,0 +1,14 @@
+package store.common.dto;
+
+public record PromotionConditionResult(
+        int applicableSetCount,
+        int stockUsed,
+        int remainingQuantity,
+        String message
+) {
+    public static PromotionConditionResult from(
+            int applicableSetCount, int stockUsed, int remainingQuantity, String message
+    ) {
+        return new PromotionConditionResult(applicableSetCount, stockUsed, remainingQuantity, message);
+    }
+}

리뷰: 앗 알고는 있었는데 놓쳤네요..🥲 감사합니다!!
@@ -0,0 +1,24 @@
+package store.common.dto;
+
+import java.util.Map;
+import java.util.Set;
+
+public record PurchaseRequest(
+        Map<String, Integer> items
+) {
+    public static PurchaseRequest from(Map<String, Integer> cart) {
+        return new PurchaseRequest(cart);
+    }
+
+    public PurchaseProductNames getProductNames() {
+        return PurchaseProductNames.from(items.keySet());
+    }
+
+    public record PurchaseProductNames(
+            Set<String> productNames
+    ) {
+        public static PurchaseProductNames from(Set<String> productNames) {
+            return new PurchaseProductNames(productNames);
+        }
+    }
+}

리뷰: 상품 이름 목록을 파라미터와 반환값으로 사용하게 되는 경우가 좀 있어서 dto 에 내부 클래스로 정의해 사용했습니다!
근데 좀 통일성이 없다는 느낌이 드네요,,
@@ -0,0 +1,34 @@
+package store.common.util;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.IntStream;
+
+public class StringUtils {
+
+    public static int parseInt(String message) {
+        try {
+            return Integer.parseInt(message);
+        } catch (NumberFormatException e) {
+            throw new IllegalArgumentException();
+        }
+    }
+
+    public static List<String> splitWithDelimiter(String message, String delimiter) {
+        return Arrays.stream(message.split(delimiter))
+                .map(String::strip)
+                .toList();
+    }
+
+    public static List<String> extractFromRegex(String message, Pattern regex) {
+        Matcher matcher = regex.matcher(message);
+        if (!matcher.matches()) {
+            throw new IllegalArgumentException();
+        }
+        return IntStream.range(1, matcher.groupCount() + 1)
+                .mapToObj(matcher::group)
+                .toList();
+    }
+}

리뷰: 아 넵! 어차피 해당 메서드를 사용하는 곳에서 catch 하고 다시 예외를 생성해서 굳이 사용하지 않을 에러 메시지를 정의 해야할까 고민하다가 그냥 비워뒀는데, 지금 보니까 그럴거면 그냥 이 메서드에서는 예외를 잡지 않았으면 됐겠네요..!

급하게 구현하느라 이상한 부분이 많은데 찾아주셔서 감사합니다 😭🙇
@@ -0,0 +1,65 @@
+package store.model;
+
+import java.util.List;
+import store.common.dto.PromotionResult;
+
+public class Receipt {
+
+    private final Cart cart;
+    private final List<PromotionResult> promotionResults;
+    private final int totalPrice;
+    private final int promotionDiscount;
+    private final int membershipDiscount;
+    private final int paymentPrice;
+
+    private Receipt(
+            Cart cart,
+            List<PromotionResult> promotionResults,
+            int totalPrice,
+            int promotionDiscount,
+            int membershipDiscount,
+            int paymentPrice
+    ) {
+        this.cart = cart;
+        this.promotionResults = promotionResults;
+        this.totalPrice = totalPrice;
+        this.promotionDiscount = promotionDiscount;
+        this.membershipDiscount = membershipDiscount;
+        this.paymentPrice = paymentPrice;
+    }
+
+    public static Receipt of(Cart cart, List<PromotionResult> promotionResults, int membershipDiscount) {
+        int totalPrice = cart.cart().entrySet().stream()
+                .mapToInt(entry -> entry.getKey().price() * entry.getValue())
+                .sum();
+        int promotionDiscount = promotionResults.stream()
+                .mapToInt(PromotionResult::discountAmount)
+                .sum();
+        return new Receipt(cart, promotionResults, totalPrice, promotionDiscount, membershipDiscount,
+                totalPrice - promotionDiscount - membershipDiscount);
+    }
+
+    public Cart cart() {
+        return cart;
+    }
+
+    public List<PromotionResult> promotionResults() {
+        return promotionResults;
+    }
+
+    public int totalPrice() {
+        return totalPrice;
+    }
+
+    public int promotionDiscount() {
+        return promotionDiscount;
+    }
+
+    public int membershipDiscount() {
+        return membershipDiscount;
+    }
+
+    public int paymentPrice() {
+        return paymentPrice;
+    }
+}

리뷰: 오호 totalPrice 와 promotionDiscount 를 계산하는 부분을 분리할 수 있겠네요! 감사합니다!!
@@ -0,0 +1,60 @@
+package store.model.promotion;
+
+import static store.common.constant.PromotionNotice.DEFAULT_NOTICE;
+import static store.common.constant.PromotionNotice.GET_FREE_M_NOTICE;
+import static store.common.constant.PromotionNotice.OUT_OF_STOCK_NOTICE;
+
+import store.common.dto.PromotionConditionResult;
+import store.common.dto.PromotionResult;
+import store.model.Product;
+
+public class BuyNGetMFreePromotion implements PromotionStrategy {
+
+    private final int purchaseQuantity;
+    private final int freeQuantity;
+
+    public BuyNGetMFreePromotion(int purchaseQuantity, int freeQuantity) {
+        this.purchaseQuantity = purchaseQuantity;
+        this.freeQuantity = freeQuantity;
+    }
+
+    @Override
+    public PromotionResult applyPromotion(Product product, int quantity) {
+        int totalFreeQuantity = getApplicableSetCount(quantity) * freeQuantity;
+        return PromotionResult.of(product.name(), product.price() * totalFreeQuantity, totalFreeQuantity);
+    }
+
+    @Override
+    public PromotionConditionResult checkCondition(Product product, int quantity) {
+        int setCountFromRequest = getApplicableSetCount(quantity);
+        int setCountFromStock = getApplicableSetCount(product.stock());
+        int applicableSetCount = Math.min(setCountFromRequest, setCountFromStock);
+        int stockUsed = applicableSetCount * getPromotionSetSize();
+        int remainingQuantity = quantity - stockUsed;
+
+        String message = getConditionMessage(product, product.stock() - stockUsed, remainingQuantity);
+        return PromotionConditionResult.from(applicableSetCount, stockUsed, remainingQuantity, message);
+    }
+
+    private String getConditionMessage(Product product, int remainingStock, int remainingQuantity) {
+        if (remainingQuantity == purchaseQuantity && remainingStock > purchaseQuantity) {
+            return GET_FREE_M_NOTICE.message(product.name(), freeQuantity);
+        }
+        if (remainingQuantity >= getPromotionSetSize()) {
+            return OUT_OF_STOCK_NOTICE.message(product.name(), remainingQuantity);
+        }
+        return DEFAULT_NOTICE.message();
+    }
+
+    private int getPromotionSetSize() {
+        return purchaseQuantity + freeQuantity;
+    }
+
+    private int getApplicableSetCount(int quantity) {
+        return quantity / getPromotionSetSize();
+    }
+
+    private int getRemainingQuantity(int quantity) {
+        return quantity % getPromotionSetSize();
+    }
+}

리뷰: 아 넵! 이 부분은 인터페이스로 확장성 있게 만들어 보고 싶었는데, 구현은 모두 요구사항 범위 내에서만 했습니다
@@ -0,0 +1,44 @@
+## Default
+- [x] 아키텍처 준수를 위한 애플리케이션 패키지 설계
+- [x] 특강 도메인 테이블 설계 및 목록/신청 등 기본 기능 구현
+- [x] 각 기능에 대한 **단위 테스트** 작성
+
+---
+### 세부 API
+- [x] 특강 선택 API (=신청 가능 강의 목록 조회 API)
+  * 날짜별로 현재 신청 가능한 특강 목록을 조회하는 API 를 작성합니다.
+  * 특강의 정원은 30명으로 고정이며, 사용자는 각 특강에 신청하기전 목록을 조회해볼 수 있어야 합니다.
+
+  **단위 테스트**
+  - [x] 파라미터 validator
+  - [x] 요청된 날짜에 해당되는 강의가 없으면 예외발생
+  - [x] 요청된 날짜에 해당되는 강의 목록 조회
+
+  > (요청한 날짜에 있는 강의 + "신청가능 인원수 = 0초과") 강의 목록 조회
+
+<br>
+
+- [x] 특강 신청 완료 목록 조회 API
+  * 특정 userId 로 신청 완료된 특강 목록을 조회하는 API 를 작성합니다.
+  * 각 항목은 특강 ID 및 이름, 강연자 정보를 담고 있어야 합니다.
+
+  **단위 테스트**
+  - [x] 파라미터 validator
+  - [x] 유저 유무
+
+<br>
+
+- [x] 특강 신청 API
+  * 특정 userId 로 선착순으로 제공되는 특강을 신청하는 API 를 작성합니다.
+  * 동일한 신청자는 동일한 강의에 대해서 한 번의 수강 신청만 성공할 수 있습니다.
+  * 특강은 선착순 30명만 신청 가능합니다.
+  * 이미 신청자가 30명이 초과되면 이후 신청자는 요청을 실패합니다.
+
+  **단위 테스트**
+  - [x] 파라미터 validator
+  - [x] 유저가 없으면 예외 발생
+  - [x] 강의가 없으면 예외 발생
+  - [x] 중복 신청 예외발생 (동일한 신청자는 동일한 강의에 대해서 한 번의 수강 신청만 가능)
+  - [x] 남은 자리가 없으면 예외발생 (30명이 초과된 상태)
+  - [x] (신청 가능한 자리수 - 1)이 정상 동작하는지 확인
+  - [x] (신청 가능한 자리수 - 1) 도중 자리수가 0 일때 예외 발생
\ No newline at end of file

리뷰: 체크 리스트를 만들어 주신 점 멋지십니다!
@@ -0,0 +1,38 @@
+# 패키지 설계
+
+```text
+├── HanghaeplusSignupForLectureApplication.java
+├── api
+│   ├── controller
+│   └── dto
+│       ├── request
+│       └── response
+├── application
+│   ├── lecture
+│   │   ├── domain
+│   │   │   ├── model
+│   │   │   └── repository
+│   │   ├── dto
+│   │   │   ├── request
+│   │   │   └── response
+│   │   ├── facade
+│   │   ├── service
+│   │   └── validator
+│   └── user
+│       ├── domain
+│       │   ├── model
+│       │   └── repository
+│       ├── service
+│       └── validator
+└── infrastructure
+    ├── lecture
+    │   ├── entity
+    │   └── repository
+    │       ├── impl
+    │       └── jpa
+    └── user
+        ├── entity
+        └── repository
+            ├── impl
+            └── jpa
+```

리뷰: application 과 domain 과 분리되어야 할 것 같아요.
@@ -0,0 +1,26 @@
+package store.util.constants;
+
+public class ViewConstants {
+    // inputView
+    public static final String INPUT_ITEM_MESSAGE = "구매하실 상품명과 수량을 입력해 주세요. (예: [사이다-2],[감자칩-1])";
+    public static final String ITEM_INPUT_FORMAT = "^\\[(\\w|[가-힣]|.)+-\\d+\\](,\\[(\\w|[가-힣]|.)+-\\d+\\])*$";
+    public static final String ERROR_INPUT_FORMAT = "[ERROR] 올바르지 않은 형식으로 입력했습니다. 다시 입력해 주세요.";
+    public static final String INPUT_MEMBERSHIP_YN_MESSAGE = "멤버십 할인을 받으시겠습니까? (Y/N)";
+    public static final String INPUT_CONTINUE_YN_MESSAGE = "감사합니다. 구매하고 싶은 다른 상품이 있나요? (Y/N)";
+    // outputView
+    public static final String WELCOME_MESSAGE = "안녕하세요. W편의점입니다.";
+    public static final String PRODUCT_INTRODUCE_MESSAGE = "현재 보유하고 있는 상품입니다.";
+    public static final String RECEIPT_HEADER = "\n==============W 편의점================";
+    public static final String RECEIPT_MIDDLE = "=============증\t정===============";
+    public static final String RECEIPT_BOTTOM = "====================================\n";
+    public static final String RECEIPT_HEADER_FORMAT = "%-10s %5s %15s";
+    public static final String RECEIPT_FORMAT = "%-10s %5d %15s";
+    public static final String RECEIPT_BOTTOM_FORMAT = "%-10s %20s\n";
+    public static final String RECEIPT_PRODUCT_NAME = "상품명";
+    public static final String RECEIPT_QUANTITY = "수량";
+    public static final String RECEIPT_AMOUNT = "금액";
+    public static final String RECEIPT_TOTAL_PRICE = "총구매액";
+    public static final String RECEIPT_PROMOTION_DISCOUNT = "행사할인";
+    public static final String RECEIPT_MEMBERSHIP_DISCOUNT = "멤버십할인";
+    public static final String RECEIPT_PAYMENT = "내실돈";
+}

리뷰: ViewConstants 클래스와 ServiceConstants 클래스를 별도로 정의하신 이유가 궁금합니다!
@@ -0,0 +1,26 @@
+package store.util.constants;
+
+public class ViewConstants {
+    // inputView
+    public static final String INPUT_ITEM_MESSAGE = "구매하실 상품명과 수량을 입력해 주세요. (예: [사이다-2],[감자칩-1])";
+    public static final String ITEM_INPUT_FORMAT = "^\\[(\\w|[가-힣]|.)+-\\d+\\](,\\[(\\w|[가-힣]|.)+-\\d+\\])*$";
+    public static final String ERROR_INPUT_FORMAT = "[ERROR] 올바르지 않은 형식으로 입력했습니다. 다시 입력해 주세요.";
+    public static final String INPUT_MEMBERSHIP_YN_MESSAGE = "멤버십 할인을 받으시겠습니까? (Y/N)";
+    public static final String INPUT_CONTINUE_YN_MESSAGE = "감사합니다. 구매하고 싶은 다른 상품이 있나요? (Y/N)";
+    // outputView
+    public static final String WELCOME_MESSAGE = "안녕하세요. W편의점입니다.";
+    public static final String PRODUCT_INTRODUCE_MESSAGE = "현재 보유하고 있는 상품입니다.";
+    public static final String RECEIPT_HEADER = "\n==============W 편의점================";
+    public static final String RECEIPT_MIDDLE = "=============증\t정===============";
+    public static final String RECEIPT_BOTTOM = "====================================\n";
+    public static final String RECEIPT_HEADER_FORMAT = "%-10s %5s %15s";
+    public static final String RECEIPT_FORMAT = "%-10s %5d %15s";
+    public static final String RECEIPT_BOTTOM_FORMAT = "%-10s %20s\n";
+    public static final String RECEIPT_PRODUCT_NAME = "상품명";
+    public static final String RECEIPT_QUANTITY = "수량";
+    public static final String RECEIPT_AMOUNT = "금액";
+    public static final String RECEIPT_TOTAL_PRICE = "총구매액";
+    public static final String RECEIPT_PROMOTION_DISCOUNT = "행사할인";
+    public static final String RECEIPT_MEMBERSHIP_DISCOUNT = "멤버십할인";
+    public static final String RECEIPT_PAYMENT = "내실돈";
+}

리뷰: 이번 요구사항에 InputView, OutputView 사용에 대한 내용을 보고 MVC 패턴을 적용해보고자 하였습니다. Constants가 여러 곳에서 사용되는 상수를 모아놓는 클래스이지만 너무 많은 곳에서 이를 의존하는 것은 개인적으로 좋지 않다고 생각하였습니다. 그리고 MVC가 역할에 따라 각각 Model, View, Controller로 나누어 놓은 것인데 하나의 Constants에서 모두 관리하는 게 맞을까라는 생각에 View와 Controller 단에서 사용하는 Constants를 나누게 되었습니다.
개인적인 생각으로 나눠 놓은 것이라 이게 맞는 설계인지는 잘 모르겠네요..
@@ -0,0 +1,44 @@
+package store.util;
+
+import static store.constant.ErrorMessage.INVALID_ORDER_FORMAT;
+import static store.constant.ErrorMessage.WRONG_INPUT;
+import static store.constant.OrderInfo.ORDER_PATTERN;
+import static store.constant.OrderInfo.PRODUCT_COUNT_GROUP;
+import static store.constant.OrderInfo.PRODUCT_NAME_GROUP;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.regex.Matcher;
+import store.model.order.Order;
+
+public class Converter {
+    public static int toInteger(String number) {
+        try {
+            return Integer.parseInt(number);
+        } catch (NumberFormatException e) {
+            throw new IllegalArgumentException(WRONG_INPUT.getMessage());
+        }
+    }
+
+    public static LocalDateTime toLocalDateTime(String localDate, String dateFormat) {
+        return LocalDate.parse(
+                        localDate,
+                        DateTimeFormatter.ofPattern(dateFormat)
+                )
+                .atStartOfDay();
+    }
+
+    public static Order toOrder(String order) {
+        Matcher matcher = ORDER_PATTERN.matcher(order);
+
+        if (!matcher.matches()) {
+            throw new IllegalArgumentException(INVALID_ORDER_FORMAT.getMessage());
+        }
+
+        return new Order(
+                matcher.group(PRODUCT_NAME_GROUP),
+                Converter.toInteger(matcher.group(PRODUCT_COUNT_GROUP))
+        );
+    }
+}

리뷰: util 클래스로 빼서 꼼꼼하게 예외처리 하신 것 너무 좋아요!
저는 3주차때 했다가 이번주차에 까먹어서 못했네요 ㅜ
@@ -0,0 +1,29 @@
+package store.model.product;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+public class Products {
+    private final Map<String, Product> productByName;
+
+    public Products(Map<String, Product> productByName) {
+        this.productByName = productByName;
+    }
+
+    public Products(List<Product> products) {
+        this(
+                products.stream()
+                        .collect(Collectors.toMap(Product::getName, Function.identity()))
+        );
+    }
+
+    public Product getNameWith(String toFind) {
+        return productByName.get(toFind);
+    }
+
+    public List<Product> toList() {
+        return productByName.values().stream().toList();
+    }
+}

리뷰: `Function.identity()`는 처음보네요..!
어떤 역할인지 알려주실 수 있나요?!
@@ -0,0 +1,17 @@
+package store.util;
+
+import store.view.OutputView;
+
+public interface Task<T> {
+    static <T> T retryTillNoException(Task<T> task) {
+        while (true) {
+            try {
+                return task.run();
+            } catch (IllegalArgumentException e) {
+                OutputView.println(e.getMessage());
+            }
+        }
+    }
+
+    T run();
+}

리뷰: 이렇게 반복 처리를 할 수도 있겠군요..!
저는 예외 재입력 로직이 너무 반복적이라 어떻게 처리할지 결론 못내고 끝냈는데 배워갑니다!!
@@ -0,0 +1,35 @@
+package store.view;
+
+import static store.constant.ErrorMessage.WRONG_INPUT;
+import static store.constant.OrderInfo.ORDER_DELIMITER;
+
+import camp.nextstep.edu.missionutils.Console;
+import java.util.ArrayList;
+import java.util.List;
+import store.constant.Answer;
+import store.model.order.Order;
+import store.util.Converter;
+import store.util.Spliter;
+
+public class InputView {
+    public static List<Order> readOrders() {
+        Spliter spliter = new Spliter(Console.readLine(), ORDER_DELIMITER);
+        List<Order> orders = new ArrayList<>();
+
+        while (spliter.hasMoreTokens()) {
+            String nextToken = spliter.nextToken();
+
+            orders.add(Converter.toOrder(nextToken));
+        }
+        return orders;
+    }
+
+    public static Answer readAnswer() {
+        Answer answer = Answer.getMatchingAnswer(Console.readLine());
+
+        if (answer == null) {
+            throw new IllegalArgumentException(WRONG_INPUT.getMessage());
+        }
+        return answer;
+    }
+}

리뷰: 저는 입력받을 때 필요한 프롬프트 출력도 `InputView`의 책임이라고 생각했습니다.
태양님은 `InputView`는 딱 입력의 역할만 갖는다는 생각이셨던 것 같네요!
또다른 관점이라 흥미롭습니다..!
@@ -0,0 +1,12 @@
+package store.constant;
+
+public class StoreInfo {
+    public static final String PRODUCTS_FILE = "src/main/resources/products.md";
+    public static final String PRODUCTS_DELIMITER = ",";
+    public static final String PROMOTIONS_FILE = "src/main/resources/promotions.md";
+    public static final String PROMOTIONS_DATE_FORMAT = "yyyy-MM-dd";
+    public static final String PROMOTIONS_DELIMITER = ",";
+    public static final String NO_PROMOTION = "null";
+    public static final double MEMBERSHIP_DISCOUNT_PERCENTAGE = 0.3;
+    public static final int MAX_MEMBERSHIP_DISCOUNT_AMOUNT = 8_000;
+}

리뷰: 저는 constants를 enum 없이 class로 생성하는 경우, 생성자를 private으로 돌리고 클래스도 final로 선언해 인스턴스화를 완전히 막아버리곤 합니다!
이번 과제에서는 오버 엔지니어링일 수 있지만..! 참고 차원에서 코멘트 드립니다 :)
@@ -0,0 +1,28 @@
+package store.constant;
+
+public class OutputInfo {
+    public static final String NEW_LINE = "%n";
+    public static final String WELCOME_MESSAGE = "안녕하세요. W편의점입니다.";
+    public static final String PRODUCT_START_MESSAGE = "현재 보유하고 있는 상품입니다." + NEW_LINE;
+    public static final String PRODUCT_FORMAT = "- %s %s원 %s" + NEW_LINE;
+    public static final String PROMOTION_PRODUCT_FORMAT = "- %s %s원 %s %s" + NEW_LINE;
+    public static final String NO_QUANTITY = "재고 없음";
+    public static final String QUANTITY_FORMAT = "%d개";
+    public static final String MAY_I_TAKE_YOUR_ORDER = "구매하실 상품명과 수량을 입력해 주세요. (예: [사이다-2],[감자칩-1])";
+    public static final String FREE_GETTABLE_COUNT_QUESTION =
+            "현재 %s은(는) %d개를 무료로 더 받을 수 있습니다. 추가하시겠습니까? (Y/N)" + NEW_LINE;
+    public static final String BUY_COUNT_WITHOUT_PROMOTION_QUESTION =
+            "현재 %s %d개는 프로모션 할인이 적용되지 않습니다. 그래도 구매하시겠습니까? (Y/N)" + NEW_LINE;
+    public static final String MEMBERSHIP_DISCOUNT = "멤버십 할인을 받으시겠습니까? (Y/N)";
+    public static final String RECEIPT_START_MESSAGE = "==============W 편의점================" + NEW_LINE;
+    public static final String RECEIPT_PURCHASE_INFO_START_MESSAGE = "상품명\t\t수량\t금액" + NEW_LINE;
+    public static final String RECEIPT_PURCHASE_INFO_FORMAT = "%s\t\t%d \t%s" + NEW_LINE;
+    public static final String RECEIPT_FREE_START_MESSAGE = "=============증\t정===============" + NEW_LINE;
+    public static final String RECEIPT_FREE_PRODUCT_FORMAT = "%s\t\t%d" + NEW_LINE;
+    public static final String RECEIPT_PAY_START_MESSAGE = "====================================" + NEW_LINE;
+    public static final String RECEIPT_TOTAL_PRICE_FORMAT = "총구매액\t\t%d\t%s" + NEW_LINE;
+    public static final String RECEIPT_PROMOTION_DISCOUNT_FORMAT = "행사할인\t\t\t-%s" + NEW_LINE;
+    public static final String RECEIPT_MEMBERSHIP_DISCOUNT_FORMAT = "멤버십할인\t\t\t-%s" + NEW_LINE;
+    public static final String RECEIPT_AMOUNT_OF_PAY_FORMAT = "내실돈\t\t\t %s" + NEW_LINE;
+    public static final String WANNA_BUY_MORE = "감사합니다. 구매하고 싶은 다른 상품이 있나요? (Y/N)";
+}

리뷰: 우테코에서는 상수 관리를 enum 타입으로 권장하고 있습니다!
저는 개인적으론 enum에 다른 메소드 추가가 없다면 태양님처럼 static final을 사용해 상수화 하는 걸 선호하는데..
태양님은 어떻게 생각하시는지 궁금합니다!
@@ -0,0 +1,34 @@
+package store.model.promotion;
+
+import static store.constant.ErrorMessage.INVALID_PERIOD;
+
+import java.time.LocalDateTime;
+
+public class Period {
+    private final LocalDateTime start;
+    private final LocalDateTime end;
+
+    public Period(LocalDateTime start, LocalDateTime end) {
+        validate(start, end);
+        this.start = start;
+        this.end = end;
+    }
+
+    private void validate(LocalDateTime start, LocalDateTime end) {
+        validateStartIsBeforeEnd(start, end);
+    }
+
+    private void validateStartIsBeforeEnd(LocalDateTime start, LocalDateTime end) {
+        if (!start.isBefore(end)) {
+            throw new IllegalArgumentException(INVALID_PERIOD.getMessage());
+        }
+    }
+
+    public boolean includes(LocalDateTime time) {
+        if (start.isEqual(time) || end.isEqual(time)) { // 시작과 끝 시간까지 포함
+            return true;
+        }
+
+        return time.isAfter(start) && time.isBefore(end);
+    }
+}

리뷰: 아마 프로모션 진행 기간인지 판단하는 코드를 작성하신 것 같아요!
아래처럼 단순화 해봐도 괜찮을 것 같다는 생각이 듭니다 :)
```java
return !(time.isBefore(start) || time.isAfter(end));
```
@@ -0,0 +1,36 @@
+package store.model.order;
+
+import static store.constant.ErrorMessage.WRONG_INPUT;
+
+public class Order {
+    protected final String productName;
+    protected int buyCount;
+
+    public Order(String productName, final int buyCount) {
+        validateNotNegative(buyCount);
+        this.productName = productName;
+        this.buyCount = buyCount;
+    }
+
+    private void validateNotNegative(final int buyCount) {
+        if (buyCount < 0) {
+            throw new IllegalArgumentException(WRONG_INPUT.getMessage());
+        }
+    }
+
+    public String getProductName() {
+        return productName;
+    }
+
+    public int getBuyCount() {
+        return buyCount;
+    }
+
+    public void add(final int toAdd) {
+        buyCount += toAdd;
+    }
+
+    public void cancel(final int toCancel) {
+        buyCount -= toCancel;
+    }
+}

리뷰: 이런것도 상수처리하면 더 좋을것 같아요