ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,38 @@
+package store.model;
+
+import java.util.*;
+
+public class PromotionRepository {
+    private final HashMap<String, Promotion> promotions = new HashMap<>();
+
+    public PromotionRepository(Scanner promotionScanner){
+        // 첫 줄 제외
+        promotionScanner.nextLine();
+
+        while(promotionScanner.hasNext()){
+            setPromotions(promotionScanner.nextLine());
+        }
+    }
+
+    public Promotion getPromotion(String name){
+        return promotions.get(name);
+    }
+
+    private void setPromotions(String promotionContext){
+        List<String> promotionInfo = new ArrayList<>(contextToList(promotionContext));
+        String promotionName = promotionInfo.getFirst();
+        this.promotions.put(promotionName, new Promotion(promotionInfo));
+    }
+
+    private List<String> contextToList(String context) {
+        List<String> promotionInfo = Arrays.asList(context.strip().split(","));
+        validateInfoSize(promotionInfo);
+        return promotionInfo;
+    }
+
+    private void validateInfoSize(List<String> promotionInfo) {
+        if (promotionInfo.size() != 5) {
+            throw new IllegalArgumentException("promotion 등록 형식에 문제 있음");
+        }
+    }
+}

리뷰: 이런 주석은 삭제하셔도 좋을 것 같아요! 
의미 없는 주석을 지우는 것도 컨벤션입니다 !
@@ -0,0 +1,71 @@
+package store.model;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+
+import static camp.nextstep.edu.missionutils.DateTimes.now;
+
+public class Promotion {
+    private final String name;
+    private final Integer buy;
+    private final Integer get;
+    private final LocalDate startDate;
+    private final LocalDate endDate;
+
+    public Promotion(List<String> promotionInfo) {
+        final Integer INDEX_NAME = 0;
+        final Integer INDEX_BUY = 1;
+        final Integer INDEX_GET = 2;
+        final Integer INDEX_START_DATE = 3;
+        final Integer INDEX_END_DATE = 4;
+
+        this.name = promotionInfo.get(INDEX_NAME);
+        this.buy = getNumber(promotionInfo.get(INDEX_BUY));
+        this.get = getNumber(promotionInfo.get(INDEX_GET));
+        this.startDate = LocalDate.parse(promotionInfo.get(INDEX_START_DATE), DateTimeFormatter.ISO_DATE);
+        this.endDate = LocalDate.parse(promotionInfo.get(INDEX_END_DATE), DateTimeFormatter.ISO_DATE);
+        validateDate();
+    }
+
+    public String getName() {
+        return this.name;
+    }
+
+    public Integer getFreeProductCount(Integer count) {
+        return count / (get + buy) * get;
+    }
+
+    public Integer getDiscountedProductCount(Integer count) {
+        return (count / (get + buy)) * (get + buy);
+    }
+
+    public Integer getAdditionalFreeProductCount(Integer count){
+        if(count - getDiscountedProductCount(count) == buy){
+            return get;
+        }
+        return 0;
+    }
+
+    public Boolean inPeriod() {
+        LocalDate now = now().toLocalDate();
+        return ((!now.isBefore(startDate)) && (!now.isAfter(endDate)));
+    }
+
+    private Integer getNumber(String context) {
+        validateOnlyNumber(context);
+        return Integer.parseInt(context);
+    }
+
+    private void validateOnlyNumber(String context) {
+        if (!context.matches("^[0-9]+$")) {
+            throw new IllegalArgumentException("promotion get, buy는 숫자로만 구성해야 합니다");
+        }
+    }
+
+    private void validateDate() {
+        if (endDate.isBefore(startDate)) {
+            throw new IllegalArgumentException("promotion 마감일이 시작일보다 빠를 수 없습니다.");
+        }
+    }
+}

리뷰: 메시지는 따로 enum으로 처리하면 추후에 관리하기 편할 것 같아요!
@@ -0,0 +1,71 @@
+package store.model;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+
+import static camp.nextstep.edu.missionutils.DateTimes.now;
+
+public class Promotion {
+    private final String name;
+    private final Integer buy;
+    private final Integer get;
+    private final LocalDate startDate;
+    private final LocalDate endDate;
+
+    public Promotion(List<String> promotionInfo) {
+        final Integer INDEX_NAME = 0;
+        final Integer INDEX_BUY = 1;
+        final Integer INDEX_GET = 2;
+        final Integer INDEX_START_DATE = 3;
+        final Integer INDEX_END_DATE = 4;
+
+        this.name = promotionInfo.get(INDEX_NAME);
+        this.buy = getNumber(promotionInfo.get(INDEX_BUY));
+        this.get = getNumber(promotionInfo.get(INDEX_GET));
+        this.startDate = LocalDate.parse(promotionInfo.get(INDEX_START_DATE), DateTimeFormatter.ISO_DATE);
+        this.endDate = LocalDate.parse(promotionInfo.get(INDEX_END_DATE), DateTimeFormatter.ISO_DATE);
+        validateDate();
+    }
+
+    public String getName() {
+        return this.name;
+    }
+
+    public Integer getFreeProductCount(Integer count) {
+        return count / (get + buy) * get;
+    }
+
+    public Integer getDiscountedProductCount(Integer count) {
+        return (count / (get + buy)) * (get + buy);
+    }
+
+    public Integer getAdditionalFreeProductCount(Integer count){
+        if(count - getDiscountedProductCount(count) == buy){
+            return get;
+        }
+        return 0;
+    }
+
+    public Boolean inPeriod() {
+        LocalDate now = now().toLocalDate();
+        return ((!now.isBefore(startDate)) && (!now.isAfter(endDate)));
+    }
+
+    private Integer getNumber(String context) {
+        validateOnlyNumber(context);
+        return Integer.parseInt(context);
+    }
+
+    private void validateOnlyNumber(String context) {
+        if (!context.matches("^[0-9]+$")) {
+            throw new IllegalArgumentException("promotion get, buy는 숫자로만 구성해야 합니다");
+        }
+    }
+
+    private void validateDate() {
+        if (endDate.isBefore(startDate)) {
+            throw new IllegalArgumentException("promotion 마감일이 시작일보다 빠를 수 없습니다.");
+        }
+    }
+}

리뷰: 해당 문자열은 따로 상수 처리하면 좋을 것 같아요!
@@ -0,0 +1,38 @@
+package store.model;
+
+import java.util.*;
+
+public class PromotionRepository {
+    private final HashMap<String, Promotion> promotions = new HashMap<>();
+
+    public PromotionRepository(Scanner promotionScanner){
+        // 첫 줄 제외
+        promotionScanner.nextLine();
+
+        while(promotionScanner.hasNext()){
+            setPromotions(promotionScanner.nextLine());
+        }
+    }
+
+    public Promotion getPromotion(String name){
+        return promotions.get(name);
+    }
+
+    private void setPromotions(String promotionContext){
+        List<String> promotionInfo = new ArrayList<>(contextToList(promotionContext));
+        String promotionName = promotionInfo.getFirst();
+        this.promotions.put(promotionName, new Promotion(promotionInfo));
+    }
+
+    private List<String> contextToList(String context) {
+        List<String> promotionInfo = Arrays.asList(context.strip().split(","));
+        validateInfoSize(promotionInfo);
+        return promotionInfo;
+    }
+
+    private void validateInfoSize(List<String> promotionInfo) {
+        if (promotionInfo.size() != 5) {
+            throw new IllegalArgumentException("promotion 등록 형식에 문제 있음");
+        }
+    }
+}

리뷰: 프로모션 입력 사이즈도 체크하셨군요! 배워갑니다 👍
@@ -0,0 +1,38 @@
+package lotto.controller.validator
+
+class PurchaseCostValidator {
+    operator fun invoke(input: String) {
+        isEmpty(input)
+        isInteger(input)
+        isZero(input)
+        isNegativeNumber(input)
+        isBetween(input)
+        isUnits(input)
+    }
+
+    private fun isEmpty(input: String) {
+        require(input.isNotEmpty()) { PurchaseErrorType.EMPTY_INPUT }
+    }
+
+    private fun isInteger(input: String) {
+        requireNotNull(input.toIntOrNull()) { PurchaseErrorType.NOT_INTEGER }
+    }
+
+    private fun isZero(input: String) {
+        require(input.toInt() != 0) { PurchaseErrorType.ZERO }
+    }
+
+    private fun isNegativeNumber(input: String) {
+        require(input.toInt() > 0) {
+            PurchaseErrorType.NEGATIVE_NUMBER
+        }
+    }
+
+    private fun isBetween(input: String) {
+        require(input.toInt() >= 1000) { PurchaseErrorType.BETWEEN }
+    }
+
+    private fun isUnits(input: String) {
+        require(input.toInt() % 1000 == 0) { PurchaseErrorType.NO_UNITS }
+    }
+}
\ No newline at end of file

리뷰: 전 빨리 풀려고 하다보니 1000원 이하나 음수에 대한 입력은
1000원 미만 금액 입력시 에러 발생하는 식으로 퉁쳤는데
역시 꼼꼼하십니다 👍
@@ -0,0 +1,40 @@
+package lotto.controller.validator
+
+class LottoNumbersValidator {
+    operator fun invoke(input: String) {
+        isEmpty(input)
+        val numbers = input.split(",").map { it.trim() }
+        isEmpty(numbers)
+        isInteger(numbers)
+        isBetween(numbers)
+        isDuplicate(numbers)
+        isSix(numbers)
+    }
+
+    private fun isEmpty(input: String) {
+        require(input.isNotEmpty()) { LottoNumbersErrorType.EMPTY_INPUT }
+    }
+
+    private fun isEmpty(numbers: List<String>) {
+        require(numbers.all { it.isNotEmpty() }) { LottoNumbersErrorType.EMPTY_INPUT }
+    }
+
+    private fun isInteger(numbers: List<String>) {
+        require(numbers.all { it.toIntOrNull() != null }) { LottoNumbersErrorType.NOT_INTEGER }
+    }
+
+    private fun isBetween(numbers: List<String>) {
+        val nums = numbers.map { it.toInt() }
+        require(nums.all { it in 1..45 }) { LottoNumbersErrorType.BETWEEN_1_45 }
+    }
+
+    private fun isDuplicate(numbers: List<String>) {
+        val nums = numbers.map { it.toInt() }
+        require(nums.size == nums.toSet().size) { LottoNumbersErrorType.DUPLICATE }
+    }
+
+    private fun isSix(numbers: List<String>) {
+        val nums = numbers.map { it.toInt() }
+        require(nums.size == 6) { LottoNumbersErrorType.SIX }
+    }
+}
\ No newline at end of file

리뷰: 혹시 String Type 에서 Int로 변형하신 이유가 있으실까용?
변형하지 않아도 원소 개수 비교하는덴 영향이 없을 것 같다고 생각해요 !
@@ -0,0 +1,54 @@
+package lotto.model
+
+import lotto.constant.WinnerType
+import lotto.util.RandomNumbers
+
+data class MyLotto(
+    private val purchaseCost: Int,
+    private val _lottos: MutableList<Lotto> = mutableListOf(),
+    private val _winners: MutableList<WinnerType> = mutableListOf(),
+) {
+    val lottoCount: Int
+        get() = purchaseCost / 1000
+
+    val lottos: List<Lotto>
+        get() = _lottos
+
+    val winners: List<WinnerType>
+        get() = _winners
+
+    val rateOfReturn: String
+        get() {
+            val totalPrizeAmount = _winners.sumOf { it.prizeAmount }
+            val rate = totalPrizeAmount / purchaseCost.toDouble() * 100
+            return String.format("%.1f", rate)
+        }
+
+    init {
+        generateLottos()
+    }
+
+    private fun generateLottos() {
+        val randomNumbers = RandomNumbers()
+        for (count in 1..lottoCount) {
+            val nums = randomNumbers()
+            _lottos.add(Lotto(nums))
+        }
+    }
+
+    fun getWinner(winningNumber: Lotto, bonusNumber: Int) {
+        _lottos.forEach { lotto ->
+            val matchCount = lotto.winningNumbers.intersect(winningNumber.winningNumbers).size
+            val hasBonus = bonusNumber in lotto.winningNumbers
+            val winner = when {
+                matchCount == 6 -> WinnerType.FIRST
+                matchCount == 5 && hasBonus -> WinnerType.SECOND
+                matchCount == 5 -> WinnerType.THIRD
+                matchCount == 4 -> WinnerType.FOURTH
+                matchCount == 3 -> WinnerType.FIFTH
+                else -> WinnerType.NONE
+            }
+            _winners.add(winner)
+        }
+    }
+}
\ No newline at end of file

리뷰: 대박 이렇게 하면 lottoCount를 외부에서 계산해주지 않아도 되는군요 !
하나 배워갑니다 ㅎㅎ
@@ -0,0 +1,54 @@
+package lotto.model
+
+import lotto.constant.WinnerType
+import lotto.util.RandomNumbers
+
+data class MyLotto(
+    private val purchaseCost: Int,
+    private val _lottos: MutableList<Lotto> = mutableListOf(),
+    private val _winners: MutableList<WinnerType> = mutableListOf(),
+) {
+    val lottoCount: Int
+        get() = purchaseCost / 1000
+
+    val lottos: List<Lotto>
+        get() = _lottos
+
+    val winners: List<WinnerType>
+        get() = _winners
+
+    val rateOfReturn: String
+        get() {
+            val totalPrizeAmount = _winners.sumOf { it.prizeAmount }
+            val rate = totalPrizeAmount / purchaseCost.toDouble() * 100
+            return String.format("%.1f", rate)
+        }
+
+    init {
+        generateLottos()
+    }
+
+    private fun generateLottos() {
+        val randomNumbers = RandomNumbers()
+        for (count in 1..lottoCount) {
+            val nums = randomNumbers()
+            _lottos.add(Lotto(nums))
+        }
+    }
+
+    fun getWinner(winningNumber: Lotto, bonusNumber: Int) {
+        _lottos.forEach { lotto ->
+            val matchCount = lotto.winningNumbers.intersect(winningNumber.winningNumbers).size
+            val hasBonus = bonusNumber in lotto.winningNumbers
+            val winner = when {
+                matchCount == 6 -> WinnerType.FIRST
+                matchCount == 5 && hasBonus -> WinnerType.SECOND
+                matchCount == 5 -> WinnerType.THIRD
+                matchCount == 4 -> WinnerType.FOURTH
+                matchCount == 3 -> WinnerType.FIFTH
+                else -> WinnerType.NONE
+            }
+            _winners.add(winner)
+        }
+    }
+}
\ No newline at end of file

리뷰: 요 클래스 감탄하면서 봤습니다 ㅎㅎ
응집도면에서 너무나 잘 설계하신 것 같아요 :)
@@ -0,0 +1,40 @@
+package lotto.controller.validator
+
+class LottoNumbersValidator {
+    operator fun invoke(input: String) {
+        isEmpty(input)
+        val numbers = input.split(",").map { it.trim() }
+        isEmpty(numbers)
+        isInteger(numbers)
+        isBetween(numbers)
+        isDuplicate(numbers)
+        isSix(numbers)
+    }
+
+    private fun isEmpty(input: String) {
+        require(input.isNotEmpty()) { LottoNumbersErrorType.EMPTY_INPUT }
+    }
+
+    private fun isEmpty(numbers: List<String>) {
+        require(numbers.all { it.isNotEmpty() }) { LottoNumbersErrorType.EMPTY_INPUT }
+    }
+
+    private fun isInteger(numbers: List<String>) {
+        require(numbers.all { it.toIntOrNull() != null }) { LottoNumbersErrorType.NOT_INTEGER }
+    }
+
+    private fun isBetween(numbers: List<String>) {
+        val nums = numbers.map { it.toInt() }
+        require(nums.all { it in 1..45 }) { LottoNumbersErrorType.BETWEEN_1_45 }
+    }
+
+    private fun isDuplicate(numbers: List<String>) {
+        val nums = numbers.map { it.toInt() }
+        require(nums.size == nums.toSet().size) { LottoNumbersErrorType.DUPLICATE }
+    }
+
+    private fun isSix(numbers: List<String>) {
+        val nums = numbers.map { it.toInt() }
+        require(nums.size == 6) { LottoNumbersErrorType.SIX }
+    }
+}
\ No newline at end of file

리뷰: 맞네요! 이게 오버 코딩을 해버렸네요 ㅎㅎ
@@ -0,0 +1,26 @@
+package lotto.constant
+
+import java.text.NumberFormat
+import java.util.*
+
+enum class WinnerType(
+    val matchingNumberCount: Int,
+    val prizeAmount: Int,
+) {
+    FIRST(6, 2_000_000_000),
+    SECOND(5, 30_000_000),
+    THIRD(5, 1_500_000),
+    FOURTH(4, 50_000),
+    FIFTH(3, 5_000),
+    NONE(0, 0);
+
+    override fun toString(): String {
+        return when (this) {
+            SECOND -> "${matchingNumberCount}개 일치, 보너스 볼 일치 (${
+                NumberFormat.getNumberInstance(Locale.KOREA).format(prizeAmount)
+            }원)"
+
+            else -> "${matchingNumberCount}개 일치 (${NumberFormat.getNumberInstance(Locale.KOREA).format(prizeAmount)}원)"
+        }
+    }
+}
\ No newline at end of file

리뷰: 와 2등일때만 print다른 것 때문에 골치아팠는데, 아예 enum에서 다르게 toString하시다니.. 또 하나 배웁니다...!
@@ -0,0 +1,38 @@
+package lotto.controller.validator
+
+class PurchaseCostValidator {
+    operator fun invoke(input: String) {
+        isEmpty(input)
+        isInteger(input)
+        isZero(input)
+        isNegativeNumber(input)
+        isBetween(input)
+        isUnits(input)
+    }
+
+    private fun isEmpty(input: String) {
+        require(input.isNotEmpty()) { PurchaseErrorType.EMPTY_INPUT }
+    }
+
+    private fun isInteger(input: String) {
+        requireNotNull(input.toIntOrNull()) { PurchaseErrorType.NOT_INTEGER }
+    }
+
+    private fun isZero(input: String) {
+        require(input.toInt() != 0) { PurchaseErrorType.ZERO }
+    }
+
+    private fun isNegativeNumber(input: String) {
+        require(input.toInt() > 0) {
+            PurchaseErrorType.NEGATIVE_NUMBER
+        }
+    }
+
+    private fun isBetween(input: String) {
+        require(input.toInt() >= 1000) { PurchaseErrorType.BETWEEN }
+    }
+
+    private fun isUnits(input: String) {
+        require(input.toInt() % 1000 == 0) { PurchaseErrorType.NO_UNITS }
+    }
+}
\ No newline at end of file

리뷰: 각 분야별로 Validator랑 ErroType이 하나씩 있는 게 파일이 깔끔해서 좋네요!
@@ -0,0 +1,36 @@
+package lotto.controller.domain
+
+import lotto.model.MyLotto
+import lotto.view.InputView
+import lotto.view.OutputView
+
+class UserInteractionController(
+    private val inputView: InputView = InputView(),
+    private val outputView: OutputView = OutputView(),
+) {
+    fun handlePurchaseCost(): String {
+        outputView.showStartMessage()
+        val purchaseCost = inputView.getInput()
+        return purchaseCost
+    }
+
+    fun handlePurchaseLotto(myLotto: MyLotto) {
+        outputView.showPurchaseLotto(myLotto)
+    }
+
+    fun handleLottoNumbers(): String {
+        outputView.showLottoNumbers()
+        val lottoNumbers = inputView.getInput()
+        return lottoNumbers
+    }
+
+    fun handleBonusNumber(): String {
+        outputView.showBonusNumber()
+        val bonusNumber = inputView.getInput()
+        return bonusNumber
+    }
+
+    fun handlePrize(myLotto: MyLotto) {
+        outputView.showPrize(myLotto)
+    }
+}
\ No newline at end of file

리뷰: LottoController외 UserController 만드는게 좋네요!
저도 입력이랑 혼재되어있어서 고민이었는데 참고해야겠습니다 ㅎㅎ
@@ -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);
+    }
+}

리뷰: 정적 팩토리 메서드 사용 시, from은 인자 값이 하나일때 사용하고 of는 인자 값이 여러개일때 주로 사용을 한다고 합니다 🤔
@@ -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);
+        }
+    }
+}

리뷰: 내장 클래스를 사용하신 의도가 궁금합니다!! 🙃
@@ -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();
+    }
+}

리뷰: 예외 처리 메세지는 일부러 보내지 않으신 걸까요?
@@ -0,0 +1,56 @@
+package store.model;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class ProductCatalog {
+
+    private final Products products;
+
+    private ProductCatalog(Products products) {
+        this.products = products;
+    }
+
+    public static ProductCatalog from(Set<Product> products) {
+        products = new HashSet<>(products);
+        Set<Product> additionalProducts = products.stream()
+                .collect(Collectors.groupingBy(Product::name))
+                .entrySet().stream()
+                .filter(entry -> entry.getValue().stream().noneMatch(p -> p.promotion() == null))
+                .map(entry -> Product.of(entry.getKey(), entry.getValue().getFirst().price(), 0, null))
+                .collect(Collectors.toSet());
+
+        products.addAll(additionalProducts);
+        return new ProductCatalog(Products.from(products));
+    }
+
+    public boolean doesContainsAllProduct(Set<String> productNames) {
+        return this.products.products().stream()
+                .map(Product::name)
+                .collect(Collectors.toSet())
+                .containsAll(productNames);
+    }
+
+    public Products getProductByName(String productName) {
+        return Products.from(
+                this.products.products().stream()
+                        .filter(product -> product.name().equals(productName))
+                        .collect(Collectors.toSet())
+        );
+    }
+
+    public Products getProductsByNames(Set<String> productNames) {
+        Set<Product> productsByName = new HashSet<>();
+        productNames.forEach(productName ->
+                productsByName.addAll(products.products().stream()
+                        .filter(product -> product.name().equals(productName))
+                        .collect(Collectors.toSet()))
+        );
+        return Products.from(productsByName);
+    }
+
+    public Set<Product> products() {
+        return products.products();
+    }
+}

리뷰: 중복 방지를 위해 Set을 사용하셨네요!! 👍
@@ -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;
+    }
+}

리뷰: 메서드를 분리해서 단일 책임 원리 원칙을 지키면 좋을 것 같아요?