ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,83 @@
+package store.domain;
+
+import java.util.Collections;
+import java.util.List;
+import store.constant.CommonValue;
+import store.constant.SignMessage;
+import store.exception.ConvenienceStoreException;
+import store.exception.ErrorMessage;
+
+public class Storage {
+    private final List<GeneralProduct> generalProducts;
+    private final List<PromotionProduct> promotionProducts;
+
+    public Storage(final List<GeneralProduct> generalProducts, final List<PromotionProduct> promotionProducts) {
+        this.generalProducts = generalProducts;
+        this.promotionProducts = promotionProducts;
+    }
+
+    public List<String> validateStorageStatus(List<String> purchaseProduct) {
+        for (String purchaseItem : purchaseProduct) {
+            List<String> item = List.of(purchaseItem.split(SignMessage.HYPHEN.getSign()));
+            validateItemName(item.get(CommonValue.ZERO.getValue()));
+            validateQuantity(item.get(CommonValue.ZERO.getValue()), item.get(CommonValue.ONE.getValue()));
+        }
+        return purchaseProduct;
+    }
+
+    public List<GeneralProduct> getGeneralProducts() {
+        return Collections.unmodifiableList(generalProducts);
+    }
+
+    public List<PromotionProduct> getPromotionProducts() {
+        return Collections.unmodifiableList(promotionProducts);
+    }
+
+    public GeneralProduct findGeneralProduct(String productName) {
+        return generalProducts.stream()
+                .filter(product -> product.getName().equals(productName))
+                .findFirst()
+                .orElse(null);
+    }
+
+    public PromotionProduct findPromotionProduct(String productName) {
+        return promotionProducts.stream()
+                .filter(product -> product.getName().equals(productName))
+                .findFirst()
+                .orElse(null);
+    }
+
+    public void subtractPromotionProduct(PromotionProduct promotionProduct, int itemQuantity) {
+        promotionProduct.subtraction(itemQuantity);
+    }
+
+    public void subtractGeneralProduct(GeneralProduct generalProduct, int itemQuantity) {
+        generalProduct.subtraction(itemQuantity);
+    }
+
+    private void validateQuantity(String name, String quantity) {
+        if (getProductQuantity(name) < Integer.parseInt(quantity)) {
+            throw ConvenienceStoreException.from(ErrorMessage.STORAGE_OVER);
+        }
+    }
+
+    private int getProductQuantity(String name) {
+        PromotionProduct promotionProduct = findPromotionProduct(name);
+        int count = CommonValue.ZERO.getValue();
+        if (promotionProduct != null) {
+            count += promotionProduct.getQuantity();
+        }
+        return count + findGeneralProduct(name).getQuantity();
+    }
+
+    private void validateItemName(String name) {
+        if (!isProductInStorage(name)) {
+            throw ConvenienceStoreException.from(ErrorMessage.NON_EXISTENT_PRODUCT);
+        }
+    }
+
+    private boolean isProductInStorage(String productName) {
+        return generalProducts.stream().anyMatch(product -> product.getName().equals(productName));
+    }
+
+}

리뷰: `Unmodifiable Collection`을 사용하셔서
외부의 변형을 막으신 점 너무 좋습니다..!
@@ -0,0 +1,16 @@
+package store.constant;
+
+public enum CommonMessage {
+    YES("Y"),
+    NO("N");
+    private final String commonMessage;
+
+    CommonMessage(final String commonMessage) {
+        this.commonMessage = commonMessage;
+    }
+
+    public String getCommonMessage() {
+        return commonMessage;
+    }
+
+}

리뷰: 감사합니다!
특정 범위의 값만 사용을 하니까 enum으로 사용해볼까? 생각해봤어요!
@@ -0,0 +1,19 @@
+package store.constant;
+
+public enum CommonValue {
+    ZERO(0),
+    ONE(1),
+    TWO(2),
+    EIGHT_THOUSAND(8000);
+
+    private final int value;
+
+    CommonValue(final int value) {
+        this.value = value;
+    }
+
+    public int getValue() {
+        return value;
+    }
+
+}

리뷰: 사실 이 부분에 대해 고민을 많이 했습니다.

상수를 enum으로 관리할지 여부를 고민했던 이유는, 해당 상수들이 특정한 값의 범위에 속하지 않고, `==`을 통한 비교를 수행하는 부분도 없었기 때문입니다. 그래서 굳이 enum을 사용할 필요가 없다고 생각했습니다.

다만, 가독성 측면에서 코드를 명확하게 만들고자 enum을 선택했는데, 실제 사용 시 `CommonValue.ZERO.getValue()`처럼 접근하는 것이 코드가 길어지고 다소 불편하게 느껴질 수 있습니다.

이 점에서 보면, 말씀하신 대로 static 상수로 관리하는 것이 더 간결하고 직관적일 것 같습니다.
@@ -0,0 +1,22 @@
+package store.constant;
+
+public enum FileMessage {
+    PRODUCTS_FILE_NAME("products.md"),
+    PROMOTION_FILE_NAME("promotions.md"),
+    NULL("null"),
+    SOFT_DRINK("탄산2+1"),
+    MD_RECOMMEND_PRODUCT("MD추천상품"),
+    FLASH_DISCOUNT("반짝할인"),
+    FILE_START_WORD("name");
+
+    private final String fileMessage;
+
+    FileMessage(final String fileMessage) {
+        this.fileMessage = fileMessage;
+    }
+
+    public String getFileMessage() {
+        return fileMessage;
+    }
+
+}

리뷰: 피드백 감사드립니다!

말씀해주신 대로, Enum은 주로 고정된 상수 값을 정의할 때 사용됩니다. 그런데 새로운 프로모션이 추가되거나 변경될 때마다 재컴파일해야 하는 불편함이 있을 것 같습니다.

이 부분은 제가 미처 고려하지 못했던 점인데, 지적해주셔서 감사드립니다!
@@ -0,0 +1,38 @@
+package store.domain;
+
+public class GeneralProduct {
+    private final String name;
+    private final String price;
+    private int quantity;
+
+    public GeneralProduct(final String name, final String price, final int quantity) {
+        this.name = name;
+        this.price = price;
+        this.quantity = quantity;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public int getQuantity() {
+        return quantity;
+    }
+
+    public String getPrice() {
+        return price;
+    }
+
+    public void subtraction(int value) {
+        this.quantity -= Math.abs(value);
+    }
+
+    @Override
+    public String toString() {
+        if (quantity == 0) {
+            return String.format("- %s %,d원 재고 없음", name, Integer.parseInt(price));
+        }
+        return String.format("- %s %,d원 %s개", name, Integer.parseInt(price), quantity);
+    }
+
+}

리뷰: 아뇨.. 특별한 이유는 없었습니다
우선 문자열로 받은 후에 추후에 int형으로 수정하려고 하였는데 까먹고 놓친 것 같아요!
말씀해주셔서 감사드립니다!
@@ -0,0 +1,40 @@
+package store.domain;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+public class Promotion {
+    private final String name;
+    private final int buy;
+    private final int get;
+    private final LocalDate startDate;
+    private final LocalDate endDate;
+
+    public Promotion(final String name, final int buy, final int get, final String startDate, final String endDate) {
+        this.name = name;
+        this.buy = buy;
+        this.get = get;
+        this.startDate = LocalDate.parse(startDate);
+        this.endDate = LocalDate.parse(endDate);
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public int getBuy() {
+        return buy;
+    }
+
+    public int getGet() {
+        return get;
+    }
+
+    public boolean isActive(LocalDateTime dateTime) {
+        LocalDate date = dateTime.toLocalDate();
+        return (date.isEqual(startDate) || date.isAfter(startDate)) &&
+                (date.isEqual(endDate) || date.isBefore(endDate));
+    }
+
+}
+

리뷰: 조건이 길어지다 보니, 날짜가 기간에 포함되는지 확인하는 메서드를 구현하는 것도 좋은 접근 같아요. `isBetween` 메서드를 사용하면 코드 가독성을 높이는 데 도움이 될 것 같네요!
@@ -0,0 +1,71 @@
+package store.domain;
+
+import java.util.ArrayList;
+import java.util.List;
+import store.constant.CommonMessage;
+import store.constant.CommonValue;
+
+public class Receipt {
+    private final double THIRTY_PERCENT = 0.3;
+    private final List<ReceiptItem> receiptItems = new ArrayList<>();
+
+    public void addItem(ReceiptItem receiptItem) {
+        receiptItems.add(receiptItem);
+    }
+
+    public List<ReceiptItem> getReceiptItems() {
+        return receiptItems;
+    }
+
+    public int totalPurchaseAmount() {
+        int sum = CommonValue.ZERO.getValue();
+        for (ReceiptItem receiptItem : receiptItems) {
+            sum += receiptItem.getTotalBuyQuantity() * receiptItem.getPrice();
+        }
+        return sum;
+    }
+
+    public int totalPromotionDiscount() {
+        int sum = CommonValue.ZERO.getValue();
+        for (ReceiptItem receiptItem : receiptItems) {
+            if (receiptItem.getGetQuantity() != CommonValue.ZERO.getValue()) {
+                sum += receiptItem.getGetQuantity() * receiptItem.getPrice();
+            }
+        }
+        return sum;
+    }
+
+    public int getTotalPurchaseCount() {
+        int sum = CommonValue.ZERO.getValue();
+        for (ReceiptItem receiptItem : receiptItems) {
+            sum += receiptItem.getTotalBuyQuantity();
+        }
+        return sum;
+    }
+
+    public int validateMembership(String userAnswer) {
+        if (userAnswer.equals(CommonMessage.YES.getCommonMessage())) {
+            return getMembershipDiscount();
+        }
+        return CommonValue.ZERO.getValue();
+    }
+
+    private int noTotalPromotionAmount() {
+        int sum = CommonValue.ZERO.getValue();
+        for (ReceiptItem receiptItem : receiptItems) {
+            if (receiptItem.getGetQuantity() == CommonValue.ZERO.getValue()) {
+                sum += receiptItem.getBuyQuantity() * receiptItem.getPrice();
+            }
+        }
+        return sum;
+    }
+
+    private int getMembershipDiscount() {
+        int membershipDiscount = (int) (noTotalPromotionAmount() * THIRTY_PERCENT);
+        if (membershipDiscount > CommonValue.EIGHT_THOUSAND.getValue()) {
+            membershipDiscount = CommonValue.EIGHT_THOUSAND.getValue();
+        }
+        return membershipDiscount;
+    }
+
+}

리뷰: 좋은 질문 감사합니다! 
Receipt 클래스를 domain 패키지에 두는 이유는 이 클래스가 애플리케이션의 핵심 도메인 로직을 처리하고 있기 때문입니다. 
Receipt는 그 자체로 구매 내역을 다루고 이를 기반으로 비즈니스 로직을 처리하는 클래스이기 때문에 도메인 계층에 위치하는 것이 자연스럽다고 생각했습니다.
물론, model이라는 별도의 패키지를 두고 관리할 수도 있지만, 현재 구조에서는 domain 패키지가 애플리케이션의 주요 비즈니스 로직을 모두 포함하고 있어서, Receipt와 같은 클래스를 별도로 분리하지 않고 도메인 내에 포함시키는 것이 코드의 일관성을 유지하는 데 도움이 된다고 판단했습니다.
@@ -0,0 +1,71 @@
+package store.domain;
+
+import java.util.ArrayList;
+import java.util.List;
+import store.constant.CommonMessage;
+import store.constant.CommonValue;
+
+public class Receipt {
+    private final double THIRTY_PERCENT = 0.3;
+    private final List<ReceiptItem> receiptItems = new ArrayList<>();
+
+    public void addItem(ReceiptItem receiptItem) {
+        receiptItems.add(receiptItem);
+    }
+
+    public List<ReceiptItem> getReceiptItems() {
+        return receiptItems;
+    }
+
+    public int totalPurchaseAmount() {
+        int sum = CommonValue.ZERO.getValue();
+        for (ReceiptItem receiptItem : receiptItems) {
+            sum += receiptItem.getTotalBuyQuantity() * receiptItem.getPrice();
+        }
+        return sum;
+    }
+
+    public int totalPromotionDiscount() {
+        int sum = CommonValue.ZERO.getValue();
+        for (ReceiptItem receiptItem : receiptItems) {
+            if (receiptItem.getGetQuantity() != CommonValue.ZERO.getValue()) {
+                sum += receiptItem.getGetQuantity() * receiptItem.getPrice();
+            }
+        }
+        return sum;
+    }
+
+    public int getTotalPurchaseCount() {
+        int sum = CommonValue.ZERO.getValue();
+        for (ReceiptItem receiptItem : receiptItems) {
+            sum += receiptItem.getTotalBuyQuantity();
+        }
+        return sum;
+    }
+
+    public int validateMembership(String userAnswer) {
+        if (userAnswer.equals(CommonMessage.YES.getCommonMessage())) {
+            return getMembershipDiscount();
+        }
+        return CommonValue.ZERO.getValue();
+    }
+
+    private int noTotalPromotionAmount() {
+        int sum = CommonValue.ZERO.getValue();
+        for (ReceiptItem receiptItem : receiptItems) {
+            if (receiptItem.getGetQuantity() == CommonValue.ZERO.getValue()) {
+                sum += receiptItem.getBuyQuantity() * receiptItem.getPrice();
+            }
+        }
+        return sum;
+    }
+
+    private int getMembershipDiscount() {
+        int membershipDiscount = (int) (noTotalPromotionAmount() * THIRTY_PERCENT);
+        if (membershipDiscount > CommonValue.EIGHT_THOUSAND.getValue()) {
+            membershipDiscount = CommonValue.EIGHT_THOUSAND.getValue();
+        }
+        return membershipDiscount;
+    }
+
+}

리뷰: 좋은 지적 감사합니다! 
말씀하신 대로 부동소수점 오차가 발생할 수 있다는 점을 간과했던 것 같습니다.
좋은 방법인 것 같아요!
@@ -0,0 +1,83 @@
+package store.domain;
+
+import java.util.Collections;
+import java.util.List;
+import store.constant.CommonValue;
+import store.constant.SignMessage;
+import store.exception.ConvenienceStoreException;
+import store.exception.ErrorMessage;
+
+public class Storage {
+    private final List<GeneralProduct> generalProducts;
+    private final List<PromotionProduct> promotionProducts;
+
+    public Storage(final List<GeneralProduct> generalProducts, final List<PromotionProduct> promotionProducts) {
+        this.generalProducts = generalProducts;
+        this.promotionProducts = promotionProducts;
+    }
+
+    public List<String> validateStorageStatus(List<String> purchaseProduct) {
+        for (String purchaseItem : purchaseProduct) {
+            List<String> item = List.of(purchaseItem.split(SignMessage.HYPHEN.getSign()));
+            validateItemName(item.get(CommonValue.ZERO.getValue()));
+            validateQuantity(item.get(CommonValue.ZERO.getValue()), item.get(CommonValue.ONE.getValue()));
+        }
+        return purchaseProduct;
+    }
+
+    public List<GeneralProduct> getGeneralProducts() {
+        return Collections.unmodifiableList(generalProducts);
+    }
+
+    public List<PromotionProduct> getPromotionProducts() {
+        return Collections.unmodifiableList(promotionProducts);
+    }
+
+    public GeneralProduct findGeneralProduct(String productName) {
+        return generalProducts.stream()
+                .filter(product -> product.getName().equals(productName))
+                .findFirst()
+                .orElse(null);
+    }
+
+    public PromotionProduct findPromotionProduct(String productName) {
+        return promotionProducts.stream()
+                .filter(product -> product.getName().equals(productName))
+                .findFirst()
+                .orElse(null);
+    }
+
+    public void subtractPromotionProduct(PromotionProduct promotionProduct, int itemQuantity) {
+        promotionProduct.subtraction(itemQuantity);
+    }
+
+    public void subtractGeneralProduct(GeneralProduct generalProduct, int itemQuantity) {
+        generalProduct.subtraction(itemQuantity);
+    }
+
+    private void validateQuantity(String name, String quantity) {
+        if (getProductQuantity(name) < Integer.parseInt(quantity)) {
+            throw ConvenienceStoreException.from(ErrorMessage.STORAGE_OVER);
+        }
+    }
+
+    private int getProductQuantity(String name) {
+        PromotionProduct promotionProduct = findPromotionProduct(name);
+        int count = CommonValue.ZERO.getValue();
+        if (promotionProduct != null) {
+            count += promotionProduct.getQuantity();
+        }
+        return count + findGeneralProduct(name).getQuantity();
+    }
+
+    private void validateItemName(String name) {
+        if (!isProductInStorage(name)) {
+            throw ConvenienceStoreException.from(ErrorMessage.NON_EXISTENT_PRODUCT);
+        }
+    }
+
+    private boolean isProductInStorage(String productName) {
+        return generalProducts.stream().anyMatch(product -> product.getName().equals(productName));
+    }
+
+}

리뷰: 감사합니다!
@@ -0,0 +1,19 @@
+package store.constant;
+
+public enum SignMessage {
+    COMMA(","),
+    LEFT_SQUARE_BRACKET("["),
+    RIGHT_SQUARE_BRACKET("]"),
+    HYPHEN("-");
+
+    private final String sign;
+
+    SignMessage(final String sign) {
+        this.sign = sign;
+    }
+
+    public String getSign() {
+        return sign;
+    }
+
+}

리뷰: 이러한 ',', '[' 등과 같이 비즈니스 로직과 관려된 부분은 따로 상수 클래스를 두는 것보다 그냥 사용 클래스 내의 상수 필드로만 두어도 될 것 같아요.

 저 같은 경우는 상수 클래스로 따로 관리하는 부분은 비즈니스와 관련된 부분을 한번에 모아보는 가독성면이 제일 크다고 생각하거든요! 자바에서는 문자열 리터럴은 따로 상수풀에서 한번에 관리하므로 저런 리터럴 부분은 클래스에 중복되어 선언되어도 메모리상에서는 문제 없으니까 사용하는 클래스의 상수 필드로 두어도 될 것 같아요.

이는 제 개인적의견이라 다른 분들의 의견도 궁금하네요!
@@ -0,0 +1,38 @@
+package store.domain;
+
+public class GeneralProduct {
+    private final String name;
+    private final String price;
+    private int quantity;
+
+    public GeneralProduct(final String name, final String price, final int quantity) {
+        this.name = name;
+        this.price = price;
+        this.quantity = quantity;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public int getQuantity() {
+        return quantity;
+    }
+
+    public String getPrice() {
+        return price;
+    }
+
+    public void subtraction(int value) {
+        this.quantity -= Math.abs(value);
+    }
+
+    @Override
+    public String toString() {
+        if (quantity == 0) {
+            return String.format("- %s %,d원 재고 없음", name, Integer.parseInt(price));
+        }
+        return String.format("- %s %,d원 %s개", name, Integer.parseInt(price), quantity);
+    }
+
+}

리뷰: 저도 이러한 2주차 땐가? 객체 출력과 관련된 부분은 해당 객체 안에서 하는게 코드도 깔끔하고 좋지않을까하여 이렇게 작성한 적이있습니다!
하지만 결국 마지막 쯤 toStirng과 관련된 코드는 모두 지우고 view에서만 담하도록 했었는데요, 그 이유로 첫 번째는 toString은 웬만하면 로그 확인용으로만 사용된다고 들었습니다. 3주차 공통 피드백에도 이에 대한 사항이 적혀있더라구요! 두 번째로 출력형식이 바뀌면 View 뿐만아니라 관련 클래스들을 찾으러 다니면서 고쳐야할 것 같다는 생각에 출력관련은 오직 View의 책임으로만 주었었습니다! 

저도 갈팡질팡 했던 사항이라 이에 대한 보욱님 의견도 궁금하네요😊
@@ -0,0 +1,34 @@
+package store.view;
+
+public enum Sentence {
+
+    PRODUCT_SELECT_STATEMENT("구매하실 상품명과 수량을 입력해 주세요. (예: [사이다-2],[감자칩-1])"),
+    HELLO_STATEMENT("안녕하세요. W편의점입니다."),
+    CURRENT_PRODUCTS_STATEMENT("현재 보유하고 있는 상품입니다."),
+    NUMBER_FORMAT("#,###"),
+    PRODUCT_FORMAT("- %s %s원 "),
+    QUANTITY_FORMAT("%s개 "),
+    OUT_OF_STOCK("재고 없음 "),
+    ADDITIONAL_PURCHASE_FORMAT("현재 %s은(는) %d개를 무료로 더 받을 수 있습니다. 추가하시겠습니까? (Y/N)"),
+    NO_PROMOTION_STATEMENT("현재 %s %d개는 프로모션 할인이 적용되지 않습니다. 그래도 구매하시겠습니까? (Y/N)"),
+    MEMBERSHIP_STATEMENT("멤버십 할인을 받으시겠습니까? (Y/N)"),
+    START_OF_RECEIPT("==============W 편의점================"),
+    HEADER_OF_PURCHASE("상품명\t\t수량\t금액"),
+    PURCHASE_FORMAT("%s\t\t%s \t%s"),
+    HEADER_OF_GIVEN("=============증\t정==============="),
+    GIVEN_FORMAT("%s\t\t%s"),
+    DIVIDE_LINE("===================================="),
+    TOTAL_PRICE_FORMAT("총구매액\t\t%s\t%s"),
+    PROMOTION_DISCOUNT_FORMAT("행사할인\t\t\t-%s"),
+    MEMBERSHIP_DISCOUNT_FORMAT("멤버십할인\t\t\t-%s"),
+    PAY_PRICE_FORMAT("내실돈\t\t\t %s"),
+    PURCHASE_AGAIN_STATEMENT("감사합니다. 구매하고 싶은 다른 상품이 있나요? (Y/N)"),
+    BLANK(""),
+    NEW_LINE(System.lineSeparator());
+
+    final String message;
+
+    Sentence(final String message) {
+        this.message = message;
+    }
+}

리뷰: 단순한 출력 문구들도 상수화 해야 하는지 살짝 의문입니다. 상수화의 이점은 중복을 제거하고 가독성을 향상 시키는 등의 효과가 있지만 불필요한 복잡성 증가, 코드 유지 관리의 어려움 등의 단점도 있습니다. 위 문구를 상수화 했을 때 정확히 어떤 도움이 되는지 다시 생각해보시면 좋을 것 같아요
@@ -0,0 +1,52 @@
+package store.view;
+
+import store.domain.Product;
+import store.domain.Products;
+import store.dto.ReceiptDto;
+
+public class View {
+
+    private final InputView inputView;
+    private final OutputView outputView;
+
+    public View(InputView inputView, OutputView outputView) {
+        this.inputView = inputView;
+        this.outputView = outputView;
+    }
+
+    public String requestProductSelect() {
+        return inputView.requestProductSelect();
+    }
+
+    public String askAdditionalPurchase(Product product) {
+        return inputView.askAdditionalPurchase(product);
+    }
+
+    public String askNoPromotion(Product product, int shortageQuantity) {
+        return inputView.askNoPromotion(product, shortageQuantity);
+    }
+
+    public String askMembership() {
+        return inputView.askMembership();
+    }
+
+    public String askPurchaseAgain() {
+        return inputView.askPurchaseAgain();
+    }
+
+    public void printHello() {
+        outputView.printHello();
+    }
+
+    public void printCurrentProducts(Products products) {
+        outputView.printCurrentProducts(products);
+    }
+
+    public void printReceipt(ReceiptDto receiptDto) {
+        outputView.printReceipt(receiptDto);
+    }
+
+    public void printBlank() {
+        outputView.printBlank();
+    }
+}

리뷰: 좋은 점:
InputView와 OutputView를 View 클래스에서 묶어주는 구조는 각 뷰 객체에 대한 의존성을 줄이고, View 클래스가 하나의 통합된 역할을 하게 만들어주는 점에서 좋은 접근이에요.

개선 사항:
하지만 현재 View 클래스는 사실상 InputView와 OutputView의 메서드를 그대로 전달하는 역할만 하고 있어요. 이렇게 전달하는 방식만으로는 View 클래스의 존재 이유가 조금 불명확할 수 있습니다. 만약 이 클래스가 다른 역할을 수행하지 않는다면, 중간에서 단순히 메서드 전달만 하는 구조는 불필요한 오버헤드를 만들 수 있어요. 예를 들어, InputView와 OutputView를 각각 호출하는 것과 동일한 방식으로 동작할 수 있으므로, 두 클래스의 역할이 명확하다면 View 클래스를 생략할 수도 있겠네요.

만약 View 클래스에서 향후 추가적인 로직이나 처리가 필요하다면, 그때 역할을 명확히 정의하는 것도 좋은 방법일 것 같습니다.
@@ -0,0 +1,28 @@
+package store.dto;
+
+import store.domain.Product;
+import store.domain.PurchaseItem;
+
+public record GivenItemDto(
+        String name,
+        int freeQuantity,
+        int price) {
+
+    public static GivenItemDto from(PurchaseItem purchaseItem) {
+        Product product = purchaseItem.getProduct();
+        int freeQuantity = calculateFreeQuantity(purchaseItem);
+
+        return new GivenItemDto(product.name(), freeQuantity, product.price());
+    }
+
+    private static int calculateFreeQuantity(PurchaseItem purchaseItem) {
+        Product product = purchaseItem.getProduct();
+
+        if (product.promotion() != null) {
+            return product.promotion()
+                    .calculateFreeQuantity(purchaseItem.getQuantity(), product.stock().getPromotionStock());
+        }
+
+        return 0;
+    }
+}

리뷰: DTO를 사용하시는 방식이 너무 좋네요
저도 이런식으로 구현했다면 한결 깔끔했을 것 같아요
@@ -0,0 +1,42 @@
+package store.domain;
+
+public record Product(
+        String name,
+        int price,
+        Stock stock,
+        Promotion promotion) {
+
+    public int calculateTotalPrice(int quantity) {
+        return price * quantity;
+    }
+
+    public void updateStock(int purchasedQuantity) {
+        int availablePromotionStock = stock.getPromotionStock();
+        int promotionQuantity = Math.min(availablePromotionStock, purchasedQuantity);
+        int regularQuantity = purchasedQuantity - promotionQuantity;
+
+        stock.minusPromotionStock(promotionQuantity);
+        stock.minusRegularStock(regularQuantity);
+    }
+
+    public int calculatePromotionDiscount(int purchasedQuantity) {
+        if (promotion == null || !promotion.isWithinPromotionPeriod()) {
+            return 0;
+        }
+
+        int eligibleForPromotion = promotion.calculateFreeQuantity(purchasedQuantity, stock.getPromotionStock());
+        return eligibleForPromotion * price;
+    }
+
+    public int getPromotionStock() {
+        return stock.getPromotionStock();
+    }
+
+    public int getRegularStock() {
+        return stock.getRegularStock();
+    }
+
+    public int getFullStock() {
+        return getPromotionStock() + getRegularStock();
+    }
+}

리뷰: 변동성이 큰 객체인데 레코드로 구현하신 이유가 궁금합니다