Repository: woowacourse/kotlin-lotto
Pull request #135: [로또 2단계] 시아 미션 제출합니다.
Author: login: Leeyerin0210
Created: 2025-02-25T09:38:42Z | Merged: 2025-03-13T10:50:04Z

# 셀프 체크리스트

<!--
모든 항목을 점검한 후 리뷰를 요청하면, 보다 의미 있는 피드백을 받을 수 있습니다.
-->

- [x] 프로그램이 정상적으로 작동하는가?
- [ ] 모든 테스트가 통과하는가?
- [ ] 이전에 받은 피드백을 모두 반영하였는가?
- [x] 코딩 스타일 가이드를 준수하였는가?
    - [x] IDE 코드 자동 정렬을 적용하였는가?
    - [x] 린트 검사를 통과하였는가?
- [x] 프로그래밍 요구 사항을 준수하였는가?
- [x] `README.md`에 기능 목록을 정리하고 명확히 기술하였는가?

# 어떤 부분에 집중하여 리뷰해야 할까요?

<!--
학습 목표에 맞게 구현하였는지 점검하고, 피드백을 받을 준비가 되었는지 확인하세요.
정답을 묻기보다, 자신의 생각과 의사 결정 과정을 공유하며 피드백을 통해 논의하는 것이 좋습니다.
-->

객체지향적 사고와 MVC 패턴이 잘 지켜졌는지를 우선시 해서 확인해주세요!

## 코드 리뷰 커뮤니케이션

<!--
리뷰어에게 질문이 있다면, PR에 직접 코멘트를 남겨 보세요.  
특정 코드 줄에 의견을 남기면 보다 구체적인 피드백을 받을 수 있습니다.  
-->
- 이전 코드의 객체 별 협력 관계가 어느 정도 개선되었나요?
- 출력 코드를 개선하기 위해 enum에 출력 관련 정보들을 넣어뒀는데, 이러면 뷰와 도메인이 너무 강하게 결합된 것일까요? 처음엔 아에 enum을 순회하면서 출력하도록 했다가, 너무 결합된 안 좋은 코드 같아서, 다시 result을 가져와서 출력하도록 했습니다.
- 요구사항은 에러가 생겼을 때 null을 리턴하라고 했으나, null을 사용하면 후속 코드가 전부 꼬이는 것 같습니다. 그래서 잘못된 입력을 받았을 때 객체에서 null을 반환하고, 컨트롤러가 null을 확인 하면 다시 입력 받도록 하였는데, 이게 맞는 방법인지 잘 모르겠습니다. null을 받았을때 null 을 계속 가져갈 수 있도록 코드를 짜는 게 좋을까요, 아니면 null은 그 자리에서 다시 입력받아서 해결시키는게 나은 방법일까요?
- 예외 발생 시 다시 입력받도록 해서 예외 처리 테스트가 불가능한데, 이런 경우에는 출력문과 직접 비교하여 테스트하면 되는 것일까요? 
- null를 받았을 때 다시 인풋받기 위해서 컨트롤러에 null 확인 로직을 넣어놓았는데, 이 정도 로직은 괜찮을까요?

 
📌 [GitHub에서 PR에 댓글 남기는 방법](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/commenting-on-a-pull-request)

## 참고 자료

<!-- 관련 문서나 참고할 만한 자료를 추가하세요. -->

## 스크린샷

<!-- 필요하다면 스크린샷을 첨부하세요. -->

![]()



Diff:
diff --git a/README.md b/README.md
index 1d5b02578..4a76f7332 100644
--- a/README.md
+++ b/README.md
@@ -3,6 +3,20 @@
 ## 기능 요구 사항
 로또 구입 금액을 입력하면 구입 금액에 해당하는 로또를 발급해야 한다.
 로또 1장의 가격은 1000원이다.
+
+## 유의할 사항
+
+- 클래스 프로퍼티로 선언할때는 주의, 여러번 호출하거나 동시에 호출하면 꼬인다.
+- 절차지향 x 객체끼리의 관계를 작성하자
+- 객체를 어떻게 재활용할까? 혹은 의존성을 연결시킬까? 
+- private 함수를 테스트 하기 쉬운 방법 중 하나는 객체에게 역할을 넘기는 것
+- 뷰와 도메인이 결합되지 않도록 주의
+- 예외처리에 대해 너무 하루종일 생각하지 말자
+- 코드의 일관성 유지
+- 객체의 프로퍼티를 var로 해버리면 나중에 초기 유효성 검사를 무시할 가능성이 있다.
+- 함수의 이름으로 어떤 기능을 하는지 유추할 수 있도록 하자
+- 뭐든지 너무 매몰되지 말자
+- 외부에서 유효성 검사를 호출해야만 할 수 있으면 의미가 없다.
 ## 기능 목록
 ### 로또 번호
 - [x] 로또 번호는 1~45 사이이다.
@@ -11,11 +25,13 @@
 - [x] 로또 번호는 중복되지 않는다.
 - [x] 로또 번호는 오름차순으로 정렬된다.
 ### 구입금액 및 수량
-- [x] 구입 금액은 천원 단위이다.
 - [x] 구입 금액을 로또 가격으로 나누면 로또 수량이다.
-- [x] 구입 금액은 최소 천원 이상이다.
+- [x] 금액은 음수일 수 없다.
+- [ ] 구매 후 남은 금액은 마지막에 출력한다.
 ### 로또 발행
-- [x] 입력된 금액에 맞게 로또를 발행한다.
+- [x] 입력된 횟수에 맞게 수동으로 로또를 입력받는다
+- [x] 입력된 금액에 맞는 로또를 생성한다.
+- [x] 남은 금액에 맞게 자동 로또를 발행한다.
 ### 당첨 통계
 - [x] 당첨 로또 번호와 보너스 볼의 중복 여부를 확인한다.
 - [x] 당첨 로또 번호와 로또 번호를 비교하여 순위를 리턴한다.
@@ -27,5 +43,3 @@
 ### 당첨금 계산
 - [x] 당첨 총 금액을 모든 로또의 순위에 따라 계산한다
 - [x] 입력된 금액과 비교하여 수익률을 도출한다
-
-
diff --git a/src/main/kotlin/lotto/Application.kt b/src/main/kotlin/lotto/Application.kt
index a9e64b41b..0c730e97b 100644
--- a/src/main/kotlin/lotto/Application.kt
+++ b/src/main/kotlin/lotto/Application.kt
@@ -1,8 +1,12 @@
 package lotto
 
 import lotto.controller.LottoController
+import lotto.view.InputView
+import lotto.view.OutputView
 
 fun main() {
-    val lottoController = LottoController()
-    lottoController.run()
+    val inputView = InputView()
+    val outputView = OutputView()
+    val controller = LottoController(inputView, outputView)
+    controller.run()
 }
diff --git a/src/main/kotlin/lotto/controller/LottoController.kt b/src/main/kotlin/lotto/controller/LottoController.kt
index e7e4f0091..b8b85defe 100644
--- a/src/main/kotlin/lotto/controller/LottoController.kt
+++ b/src/main/kotlin/lotto/controller/LottoController.kt
@@ -1,53 +1,91 @@
 package lotto.controller
 
-import lotto.model.Amount
-import lotto.model.Lotto
-import lotto.model.LottoMachine
-import lotto.model.LottoMatcher
-import lotto.model.LottoNumber
-import lotto.model.LottoNumbers
-import lotto.model.PrizeCalculator
+import lotto.domain.model.Amount
+import lotto.domain.model.Lotto
+import lotto.domain.model.LottoCreationResult
+import lotto.domain.model.LottoNumber
+import lotto.domain.model.Rank
+import lotto.domain.service.RankCalculator
+import lotto.domain.service.WinningListMaker
 import lotto.view.InputView
+import lotto.view.Message
 import lotto.view.OutputView
 
 class LottoController(
-    private val inputView: InputView = InputView(),
-    private val outputView: OutputView = OutputView(),
+    private val inputView: InputView,
+    private val outputView: OutputView,
 ) {
+    private val amount = getAmount()
+
     fun run() {
-        val amount = getAmount()
-        val lottoMachine = LottoMachine(amount)
-        val publishedLotto = publishLotto(lottoMachine)
-        val winningLotto = getWinningLotto()
-        val bonusNumber = getBonusNumber()
-        val lottoMatcher = LottoMatcher(winningLotto, bonusNumber)
-        showEarningRate(amount, lottoMatcher, publishedLotto)
+        val count = getManualCount()
+        val manualLottoList = getLottoList(count)
+        val autoLottoList = getAutoLotto(amount.getCount(LOTTO_PRIZE) - count)
+
+        outputView.printPurchaseResult(manualLottoList, autoLottoList)
+        val winningLotto = getValidLotto { inputView.getWinningLotto() }
+        val winningNumber = getBonusNumber({ inputView.getBonusNumber() }, winningLotto)
+        val ranks = WinningListMaker(winningLotto, winningNumber).makeWinningList(manualLottoList + autoLottoList)
+        outputView.printResult(ranks, calculateEarningRate(ranks))
+    }
+
+    private fun getBonusNumber(
+        getNumber: () -> Int,
+        winningLotto: Lotto,
+    ): LottoNumber {
+        val bonusNumber = LottoNumber.valueOfOrNull(getNumber())
+        when {
+            bonusNumber == null -> outputView.printErrorMessage(Message.errorInvalidBonusNumber())
+            winningLotto.findNumber(bonusNumber) -> outputView.printErrorMessage(Message.errorWinningLottoBonusNumberDuplicated())
+            else -> return bonusNumber
+        }
+        return getBonusNumber(getNumber, winningLotto)
     }
 
-    private fun getAmount(): Amount = Amount(inputView.getMoney())
+    private fun getAutoLotto(count: Int): List<Lotto> = List(count) { Lotto.createRandom() }
 
-    private fun publishLotto(lottoMachine: LottoMachine): List<Lotto> {
-        val publishedLotto = lottoMachine.publishLottoTickets(Amount(LOTTO_PRIZE))
-        outputView.printPublishedLotto(publishedLotto)
-        return publishedLotto
+    private fun getManualCount(): Int {
+        val manualCount = inputView.getManualCount()
+        if (amount.getCount(LOTTO_PRIZE) < manualCount) {
+            outputView.printErrorMessage(Message.errorCountExceeded())
+            return getManualCount()
+        }
+        return manualCount
     }
 
-    private fun getWinningLotto(): Lotto {
-        val winningInput = inputView.getWinningLotto()
-        return Lotto(LottoNumbers(winningInput.map { number -> LottoNumber(number) }), Amount(LOTTO_PRIZE))
+    private fun getLottoList(count: Int): List<Lotto> {
+        inputView.messageManualLotto()
+        return List(count) { getValidLotto { inputView.getManualLotto() } }
     }
 
-    private fun getBonusNumber(): LottoNumber = LottoNumber(inputView.getBonusNumber())
+    private fun getValidLotto(getNumbers: () -> List<Int>): Lotto {
+        val numbers = getNumbers().mapNotNull { LottoNumber.valueOfOrNull(it) }
+        val result = Lotto.valueOf(numbers)
+        return when (result) {
+            is LottoCreationResult.Success -> result.lotto
+            is LottoCreationResult.Failure.InvalidCount -> {
+                outputView.printErrorMessage(Message.errorInvalidCount())
+                getValidLotto(getNumbers)
+            }
+            is LottoCreationResult.Failure.DuplicatedNumbers -> {
+                outputView.printErrorMessage(Message.errorDuplicatedNumbers())
+                getValidLotto(getNumbers)
+            }
+        }
+    }
+
+    private fun getAmount(): Amount {
+        val amount = Amount.valueOfOrNull(inputView.getMoney())
+        if (amount != null) return amount
+
+        outputView.printErrorMessage(Message.errorInvalidAmount())
+        return getAmount()
+    }
 
-    private fun showEarningRate(
-        amount: Amount,
-        lottoMatcher: LottoMatcher,
-        publishedLotto: List<Lotto>,
-    ) {
-        val result = lottoMatcher.matchLotto(publishedLotto)
-        val prizeCalculator = PrizeCalculator()
-        val earningRate = prizeCalculator.calculateEarningRate(amount.money, result)
-        outputView.printPrize(result, earningRate)
+    private fun calculateEarningRate(ranks: Map<Rank, Int>): Double {
+        val rankCalculator = RankCalculator()
+        val totalWinnings = rankCalculator.earningMoney(ranks)
+        return rankCalculator.calculateEarningRate(amount.money, totalWinnings)
     }
 
     companion object {
diff --git a/src/main/kotlin/lotto/domain/model/Amount.kt b/src/main/kotlin/lotto/domain/model/Amount.kt
new file mode 100644
index 000000000..38078db0e
--- /dev/null
+++ b/src/main/kotlin/lotto/domain/model/Amount.kt
@@ -0,0 +1,19 @@
+package lotto.domain.model
+
+class Amount private constructor(
+    val money: Int,
+) {
+    init {
+        require(money >= 0) { "[ERROR] 금액은 0 이상이어야 합니다." }
+    }
+
+    fun getCount(unitMoney: Int): Int = money / unitMoney
+
+    fun paymentOrNull(payMoney: Int): Amount? = runCatching { Amount(money - payMoney) }.getOrNull()
+
+    companion object {
+        fun valueOf(money: Int): Amount = Amount(money)
+
+        fun valueOfOrNull(money: Int): Amount? = runCatching { valueOf(money) }.getOrNull()
+    }
+}
diff --git a/src/main/kotlin/lotto/domain/model/Lotto.kt b/src/main/kotlin/lotto/domain/model/Lotto.kt
new file mode 100644
index 000000000..b90e2f292
--- /dev/null
+++ b/src/main/kotlin/lotto/domain/model/Lotto.kt
@@ -0,0 +1,61 @@
+package lotto.domain.model
+
+interface SortStrategy {
+    fun sort(numberList: List<LottoNumber>): List<LottoNumber>
+}
+
+class RandomSort : SortStrategy {
+    override fun sort(numberList: List<LottoNumber>): List<LottoNumber> = numberList.shuffled()
+}
+
+sealed class LottoCreationResult {
+    data class Success(
+        val lotto: Lotto,
+    ) : LottoCreationResult()
+
+    sealed class Failure : LottoCreationResult() {
+        object InvalidCount : Failure()
+
+        object DuplicatedNumbers : Failure()
+    }
+}
+
+class Lotto private constructor(
+    val numberList: List<LottoNumber>,
+) {
+    fun countMatchNumbers(lotto: Lotto): Int {
+        var count = 0
+        lotto.numberList.forEach { lottoNumber ->
+            if (findNumber(lottoNumber)) count++
+        }
+        return count
+    }
+
+    fun findNumber(lottoNumber: LottoNumber): Boolean = numberList.contains(lottoNumber)
+
+    init {
+        require(numberList.size == LOTTO_NUMBER_QUANTITY) { "[ERROR] 로또 번호는 ${LOTTO_NUMBER_QUANTITY}개여야 합니다." }
+        require(numberList.distinctBy { it.value }.size == numberList.size) { "[ERROR] 중복된 로또 번호가 있습니다." }
+    }
+
+    companion object {
+        const val LOTTO_NUMBER_QUANTITY = 6
+        private val LOTTO_NUMBERS: List<LottoNumber> = (1..45).map { LottoNumber.valueOf(it) }
+
+        fun valueOf(numberList: List<LottoNumber>): LottoCreationResult =
+            runCatching { Lotto(numberList.sortedBy { it.value }) }
+                .fold(
+                    onSuccess = { LottoCreationResult.Success(it) },
+                    onFailure = { throwable ->
+                        when (throwable.message) {
+                            "[ERROR] 로또 번호는 ${LOTTO_NUMBER_QUANTITY}개여야 합니다." -> LottoCreationResult.Failure.InvalidCount
+                            "[ERROR] 중복된 로또 번호가 있습니다." -> LottoCreationResult.Failure.DuplicatedNumbers
+                            else -> LottoCreationResult.Failure.InvalidCount
+                        }
+                    },
+                )
+
+        fun createRandom(sortStrategy: SortStrategy = RandomSort()): Lotto =
+            Lotto(sortStrategy.sort(LOTTO_NUMBERS).take(LOTTO_NUMBER_QUANTITY).sortedBy { it.value })
+    }
+}
diff --git a/src/main/kotlin/lotto/domain/model/LottoNumber.kt b/src/main/kotlin/lotto/domain/model/LottoNumber.kt
new file mode 100644
index 000000000..b10d6a969
--- /dev/null
+++ b/src/main/kotlin/lotto/domain/model/LottoNumber.kt
@@ -0,0 +1,18 @@
+package lotto.domain.model
+
+data class LottoNumber(
+    val value: Int,
+) {
+    init {
+        require(value in VALID_RANGE) { RANGE_ERROR }
+    }
+
+    companion object {
+        private val VALID_RANGE = 1..45
+        private const val RANGE_ERROR = "[ERROR] 범위 외의 값입니다."
+
+        fun valueOf(value: Int): LottoNumber = LottoNumber(value)
+
+        fun valueOfOrNull(value: Int): LottoNumber? = runCatching { valueOf(value) }.getOrNull()
+    }
+}
diff --git a/src/main/kotlin/lotto/domain/model/Rank.kt b/src/main/kotlin/lotto/domain/model/Rank.kt
new file mode 100644
index 000000000..a973bebef
--- /dev/null
+++ b/src/main/kotlin/lotto/domain/model/Rank.kt
@@ -0,0 +1,25 @@
+package lotto.domain.model
+
+enum class Rank(
+    val countOfMatch: Int,
+    val winningMoney: Int,
+    val matchBonus: Boolean,
+) {
+    FIRST(6, 2_000_000_000, false),
+    SECOND(5, 30_000_000, true),
+    THIRD(5, 1_500_000, false),
+    FOURTH(4, 50_000, false),
+    FIFTH(3, 5_000, false),
+    MISS(0, 0, false),
+    ;
+
+    companion object {
+        fun valueOf(
+            countOfMatch: Int,
+            matchBonus: Boolean,
+        ): Rank =
+            entries.firstOrNull {
+                (it.countOfMatch <= countOfMatch) && ((it.matchBonus && matchBonus) == it.matchBonus)
+            } ?: MISS
+    }
+}
diff --git a/src/main/kotlin/lotto/domain/service/RankCalculator.kt b/src/main/kotlin/lotto/domain/service/RankCalculator.kt
new file mode 100644
index 000000000..61e2c907a
--- /dev/null
+++ b/src/main/kotlin/lotto/domain/service/RankCalculator.kt
@@ -0,0 +1,21 @@
+package lotto.domain.service
+
+import lotto.domain.model.Rank
+
+class RankCalculator {
+    fun earningMoney(winningList: Map<Rank, Int>): Int {
+        var money = 0
+        winningList.forEach {
+            money += it.key.winningMoney * it.value
+        }
+        return money
+    }
+
+    fun calculateEarningRate(
+        inputMoney: Int,
+        earningMoney: Int,
+    ): Double {
+        require(inputMoney != 0) { "[ERROR] 입력 금액이 0입니다" }
+        return earningMoney.toDouble() / inputMoney.toDouble()
+    }
+}
diff --git a/src/main/kotlin/lotto/domain/service/WinningListMaker.kt b/src/main/kotlin/lotto/domain/service/WinningListMaker.kt
new file mode 100644
index 000000000..18fb41e27
--- /dev/null
+++ b/src/main/kotlin/lotto/domain/service/WinningListMaker.kt
@@ -0,0 +1,22 @@
+package lotto.domain.service
+
+import lotto.domain.model.Lotto
+import lotto.domain.model.LottoNumber
+import lotto.domain.model.Rank
+
+class WinningListMaker(
+    private val winningLotto: Lotto,
+    private val winningLottoNumber: LottoNumber,
+) {
+    fun makeWinningList(lottos: List<Lotto>): Map<Rank, Int> {
+        val ranks = lottos.map { Rank.valueOf(it.countMatchNumbers(winningLotto), it.findNumber(winningLottoNumber)) }
+        val rankCounts =
+            Rank.entries
+                .associateWith { 0 }
+                .toMutableMap()
+        ranks.forEach {
+            rankCounts[it] = rankCounts.getOrDefault(it, 0) + 1
+        }
+        return rankCounts.filter { it.key != Rank.MISS }.toSortedMap(compareByDescending { it.ordinal })
+    }
+}
diff --git a/src/main/kotlin/lotto/model/Amount.kt b/src/main/kotlin/lotto/model/Amount.kt
deleted file mode 100644
index b87d7ac33..000000000
--- a/src/main/kotlin/lotto/model/Amount.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package lotto.model
-
-data class Amount(
-    val money: Int,
-) {
-    init {
-        require(money > 0) { MONEY_UNDER_ZERO }
-    }
-
-    fun moneySplit(splitMoney: Amount): List<Amount> {
-        val moneyList: MutableList<Amount> = mutableListOf()
-        require(money % splitMoney.money == 0) { MONEY_SPILT_ERROR }
-        repeat(money / splitMoney.money) { moneyList.add(splitMoney) }
-        return moneyList
-    }
-
-    companion object {
-        const val MONEY_UNDER_ZERO = "[ERROR] 금액은 양수이어야 합니다."
-        const val MONEY_SPILT_ERROR = "[ERROR] 로또 금액에 맞게 나눌 수 없습니다"
-    }
-}
diff --git a/src/main/kotlin/lotto/model/Lotto.kt b/src/main/kotlin/lotto/model/Lotto.kt
deleted file mode 100644
index a0d2a6ad2..000000000
--- a/src/main/kotlin/lotto/model/Lotto.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package lotto.model
-
-class Lotto(
-    val numbers: LottoNumbers,
-    val prize: Amount,
-) {
-    fun getNumbers(): List<LottoNumber> = numbers.numberList.map { it }
-
-    fun findNumber(number: LottoNumber): Boolean = numbers.include(number)
-
-    fun countMatchedNumber(other: Lotto): Int = numbers.numberList.count { other.findNumber(it) }
-}
diff --git a/src/main/kotlin/lotto/model/LottoMachine.kt b/src/main/kotlin/lotto/model/LottoMachine.kt
deleted file mode 100644
index d25c78b66..000000000
--- a/src/main/kotlin/lotto/model/LottoMachine.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-package lotto.model
-
-import kotlin.collections.mutableListOf
-
-interface ShuffleStrategy {
-    fun shuffle(list: List<Int>): List<Int>
-}
-
-class NormalShuffle : ShuffleStrategy {
-    override fun shuffle(list: List<Int>): List<Int> = list.shuffled()
-}
-
-class LottoMachine(
-    private val amount: Amount,
-    private val shuffle: ShuffleStrategy = NormalShuffle(),
-) {
-    private val numbers: List<Int> = (LottoNumber.MIN_VALUE..LottoNumber.MAX_VALUE).toList()
-
-    fun publishLottoTickets(lottoPrize: Amount): List<Lotto> {
-        val lottoTickets = mutableListOf<Lotto>()
-        val moneyList = amount.moneySplit(lottoPrize)
-        repeat(moneyList.size) {
-            val lotto = publishLotto(lottoPrize)
-            lottoTickets.add(lotto)
-        }
-        return lottoTickets
-    }
-
-    private fun publishLotto(prize: Amount): Lotto {
-        val lottoNumbers =
-            shuffle
-                .shuffle(numbers)
-                .take(LOTTO_NUMBER_COUNT)
-                .sorted()
-                .map { LottoNumber(it) }
-        return Lotto(LottoNumbers(lottoNumbers), prize)
-    }
-
-    companion object {
-        private const val LOTTO_NUMBER_COUNT = 6
-    }
-}
diff --git a/src/main/kotlin/lotto/model/LottoMatcher.kt b/src/main/kotlin/lotto/model/LottoMatcher.kt
deleted file mode 100644
index db575faf9..000000000
--- a/src/main/kotlin/lotto/model/LottoMatcher.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-package lotto.model
-
-class LottoMatcher(
-    private val winningLotto: Lotto,
-    private val bonusNumber: LottoNumber,
-) {
-    init {
-        require(!winningLotto.getNumbers().contains(bonusNumber)) {
-            BONUS_DUPLICATE_MESSAGE
-        }
-    }
-
-    fun matchLotto(publishedLotto: List<Lotto>): Map<Rank, Int> {
-        val rankMap: MutableMap<Rank, Int> = enumValues<Rank>().associateWith { 0 }.toMutableMap()
-        publishedLotto
-            .groupingBy { calculateRank(it) }
-            .eachCount()
-            .forEach { (rank, count) -> rankMap[rank] = count }
-
-        return rankMap
-    }
-
-    private fun calculateRank(other: Lotto): Rank {
-        val matchCount = winningLotto.countMatchedNumber(other)
-        val matchBonus = other.findNumber(bonusNumber)
-        return Rank.valueOf(matchCount, matchBonus)
-    }
-
-    companion object {
-        private const val BONUS_DUPLICATE_MESSAGE = "[ERROR] 당첨 번호와 보너스 볼은 중복될 수 없습니다."
-    }
-}
diff --git a/src/main/kotlin/lotto/model/LottoNumber.kt b/src/main/kotlin/lotto/model/LottoNumber.kt
deleted file mode 100644
index f6071197b..000000000
--- a/src/main/kotlin/lotto/model/LottoNumber.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package lotto.model
-
-@JvmInline
-value class LottoNumber(
-    val value: Int,
-) {
-    init {
-        require(value in MIN_VALUE..MAX_VALUE) {
-            RANGE_ERROR_MESSAGE
-        }
-    }
-
-    companion object {
-        const val MIN_VALUE = 1
-        const val MAX_VALUE = 45
-        private const val RANGE_ERROR_MESSAGE = "[ERROR] 로또 번호는 1~45 사이여야 합니다."
-    }
-}
diff --git a/src/main/kotlin/lotto/model/LottoNumbers.kt b/src/main/kotlin/lotto/model/LottoNumbers.kt
deleted file mode 100644
index 4be42358c..000000000
--- a/src/main/kotlin/lotto/model/LottoNumbers.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package lotto.model
-
-class LottoNumbers(
-    val numberList: List<LottoNumber>,
-) {
-    init {
-        require(numberList.size == LOTTO_NUMBER_SIZE) {
-            SIZE_ERROR_MESSAGE
-        }
-        require(numberList.distinct().size == numberList.size) {
-            DUPLICATE_ERROR_MESSAGE
-        }
-    }
-
-    fun include(number: LottoNumber): Boolean = numberList.contains(number)
-
-    companion object {
-        const val LOTTO_NUMBER_SIZE = 6
-        private const val SIZE_ERROR_MESSAGE = "[ERROR] 로또 번호는 6개여야 합니다."
-        private const val DUPLICATE_ERROR_MESSAGE = "[ERROR] 로또 번호는 중복될 수 없습니다."
-    }
-}
diff --git a/src/main/kotlin/lotto/model/PrizeCalculator.kt b/src/main/kotlin/lotto/model/PrizeCalculator.kt
deleted file mode 100644
index aa21bf571..000000000
--- a/src/main/kotlin/lotto/model/PrizeCalculator.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package lotto.model
-
-class PrizeCalculator {
-    fun calculateInputMoney(lottoList: List<Lotto>): Int {
-        val allAmount = 0
-        lottoList.forEach {
-            allAmount + it.prize.money
-        }
-        return allAmount
-    }
-
-    fun calculateEarningRate(
-        money: Int,
-        result: Map<Rank, Int>,
-    ): Double = calculateTotalPrize(result).toDouble() / money.toDouble()
-
-    private fun calculateTotalPrize(result: Map<Rank, Int>): Int = result.entries.sumOf { (rank, count) -> rank.winningMoney * count }
-}
diff --git a/src/main/kotlin/lotto/model/Rank.kt b/src/main/kotlin/lotto/model/Rank.kt
deleted file mode 100644
index a2b3947f0..000000000
--- a/src/main/kotlin/lotto/model/Rank.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-package lotto.model
-
-enum class Rank(
-    val countOfMatch: Int,
-    val winningMoney: 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),
-    MISS(0, 0),
-    ;
-
-    companion object {
-        fun valueOf(
-            countOfMatch: Int,
-            matchBonus: Boolean,
-        ): Rank =
-            when {
-                countOfMatch == FIRST.countOfMatch -> FIRST
-                countOfMatch == SECOND.countOfMatch && matchBonus -> SECOND
-                countOfMatch == THIRD.countOfMatch -> THIRD
-                countOfMatch == FOURTH.countOfMatch -> FOURTH
-                countOfMatch == FIFTH.countOfMatch -> FIFTH
-                else -> MISS
-            }
-    }
-}
diff --git a/src/main/kotlin/lotto/view/InputView.kt b/src/main/kotlin/lotto/view/InputView.kt
index 2003a8cba..b473357ab 100644
--- a/src/main/kotlin/lotto/view/InputView.kt
+++ b/src/main/kotlin/lotto/view/InputView.kt
@@ -1,6 +1,9 @@
 package lotto.view
 
+import lotto.view.InputMessage.ALERT_INPUT_LOTTO
 import lotto.view.InputMessage.BONUS_NUMBER_INPUT_MESSAGE
+import lotto.view.InputMessage.COUNT_INPUT_MESSAGE
+import lotto.view.InputMessage.ERROR_MESSAGE
 import lotto.view.InputMessage.MONEY_INPUT_MESSAGE
 import lotto.view.InputMessage.WINNING_LOTTO_INPUT_MESSAGE
 
@@ -8,48 +11,43 @@ object InputMessage {
     const val MONEY_INPUT_MESSAGE = "구입금액을 입력해 주세요."
     const val WINNING_LOTTO_INPUT_MESSAGE = "\n지난 주 당첨 번호를 입력해 주세요."
     const val BONUS_NUMBER_INPUT_MESSAGE = "보너스 볼을 입력해 주세요."
+    const val COUNT_INPUT_MESSAGE = "\n수동으로 구매할 로또 수를 입력해 주세요."
+    const val ALERT_INPUT_LOTTO = "수동으로 구매할 번호를 입력해 주세요."
+    const val ERROR_MESSAGE = "[ERROR] 입력이 올바르지 않습니다. 다시 입력해 주세요."
 }
 
 class InputView {
-    fun getMoney(): Int = getSingleNumber(MONEY_INPUT_MESSAGE)
+    fun getMoney(): Int = getValidSingleNumber(MONEY_INPUT_MESSAGE)
 
-    fun getWinningLotto(): List<Int> = getMultipleNumber(WINNING_LOTTO_INPUT_MESSAGE)
+    fun getWinningLotto(): List<Int> = getValidMultipleNumbers(WINNING_LOTTO_INPUT_MESSAGE)
 
-    fun getBonusNumber(): Int = getSingleNumber(BONUS_NUMBER_INPUT_MESSAGE)
+    fun getBonusNumber(): Int = getValidSingleNumber(BONUS_NUMBER_INPUT_MESSAGE)
 
-    private fun getSingleNumber(message: String): Int {
-        println(message)
-        val input = readln()
-        validateSingleInput(input)
-        return input.toInt()
-    }
+    fun getManualCount(): Int = getValidSingleNumber(COUNT_INPUT_MESSAGE)
 
-    private fun getMultipleNumber(message: String): List<Int> {
-        println(message)
-        val input = readln()
-        validateMultipleInput(input)
-        return input.split(",").map { it.trim().toInt() }
-    }
+    fun messageManualLotto() = println(ALERT_INPUT_LOTTO)
 
-    private fun validateSingleInput(input: String) {
-        validateEmptyInput(input)
-        validateNumber(input)
-    }
+    fun getManualLotto(): List<Int> = getValidMultipleNumbers(null)
 
-    private fun validateMultipleInput(input: String) {
-        validateEmptyInput(input)
-        input.split(",").forEach { validateNumber(it.trim()) }
-    }
+    private fun getValidSingleNumber(message: String): Int {
+        while (true) {
+            println(message)
+            val input = readln().trim()
+            val number = input.toIntOrNull()
 
-    private fun validateEmptyInput(input: String) {
-        require(input.trim().isNotEmpty()) {
-            "[ERROR] 입력이 존재하지 않습니다."
+            if (number != null) return number
+            println(ERROR_MESSAGE)
         }
     }
 
-    private fun validateNumber(input: String) {
-        require(input.toIntOrNull() != null) {
-            "[ERROR] 입력이 정수가 아닙니다: $input"
+    private fun getValidMultipleNumbers(message: String?): List<Int> {
+        while (true) {
+            message?.let { println(it) }
+            val input = readln().trim()
+            val numbers = input.split(",").mapNotNull { it.trim().toIntOrNull() }
+
+            if (numbers.isNotEmpty() && numbers.size == input.split(",").size) return numbers
+            println(ERROR_MESSAGE)
         }
     }
 }
diff --git a/src/main/kotlin/lotto/view/OutputView.kt b/src/main/kotlin/lotto/view/OutputView.kt
index 33dfc5e3a..50bc75ab3 100644
--- a/src/main/kotlin/lotto/view/OutputView.kt
+++ b/src/main/kotlin/lotto/view/OutputView.kt
@@ -1,30 +1,66 @@
 package lotto.view
 
-import lotto.model.Lotto
-import lotto.model.Rank
+import lotto.domain.model.Lotto
+import lotto.domain.model.Rank
+
+object Message {
+    fun purchaseResult(
+        manualCount: Int,
+        autoCount: Int,
+    ) = "\n수동으로 ${manualCount}장, 자동으로 ${autoCount}장을 구매했습니다."
+
+    fun winningStatsHeader() = "\n당첨 통계\n---------"
+
+    fun winningStats(
+        rankCount: Int,
+        bonus: Boolean,
+        winningMoney: Int,
+        count: Int,
+    ): String {
+        val bonusText = if (bonus) ", 보너스 볼 일치" else ""
+        return "${rankCount}개 일치$bonusText (${winningMoney}원) - ${count}개"
+    }
+
+    fun earningRate(rate: Double) = "총 수익률은 %.2f입니다.".format(rate)
+
+    fun errorInvalidCount(): String = "[ERROR] 로또 번호는 6개여야 합니다."
+
+    fun errorDuplicatedNumbers(): String = "[ERROR] 중복된 로또 번호가 있습니다."
+
+    fun errorInvalidAmount() = "[ERROR] 금액은 0 이상이어야 합니다. 다시 입력하세요."
+
+    fun errorWinningLottoNumberSize(): String = "[ERROR] 당첨 번호는 6개여야 합니다."
+
+    fun errorWinningLottoDuplicatedNumbers(): String = "[ERROR] 당첨 번호에 중복된 숫자가 있습니다."
+
+    fun errorWinningLottoBonusNumberDuplicated(): String = "[ERROR] 보너스 번호는 당첨 번호에 포함될 수 없습니다."
+
+    fun errorInvalidBonusNumber(): String = "[ERROR] 올바르지 않은 보너스 번호입니다."
+
+    fun errorCountExceeded() = "[ERROR] 수동 로또 개수가 전체 구매 가능 개수를 초과할 수 없습니다. 다시 입력하세요."
+}
 
 class OutputView {
-    fun printPublishedLotto(publishedLotto: List<Lotto>) {
-        println("${publishedLotto.size}개를 구매했습니다.")
-        publishedLotto.forEach { lotto ->
-            val numbers = lotto.numbers.numberList.map { it.value }
-            println(numbers)
-        }
+    fun printPurchaseResult(
+        manualLottos: List<Lotto>,
+        autoLottos: List<Lotto>,
+    ) {
+        println(Message.purchaseResult(manualLottos.size, autoLottos.size))
+        autoLottos.forEach { println(it.numberList.map { num -> num.value }) }
     }
 
-    fun printPrize(
-        result: Map<Rank, Int>,
+    fun printResult(
+        sortedResults: Map<Rank, Int>,
         earningRate: Double,
     ) {
-        println()
-        println("당첨 통계")
-        println("---------")
-        result.entries.sortedBy { it.key.winningMoney }.filter { it.key != Rank.MISS }.forEach { (rank, value) ->
-            print("${rank.countOfMatch}개 일치")
-            if (rank.name == "SECOND") print(", 보너스 볼 일치")
-            print(" (${rank.winningMoney}원) - ${value}개")
-            println()
+        println(Message.winningStatsHeader())
+        sortedResults.forEach { (rank, count) ->
+            println(Message.winningStats(rank.countOfMatch, rank.matchBonus, rank.winningMoney, count))
         }
-        println("총 수익률은 ${String.format("%.2f", earningRate)}입니다.")
+        println(Message.earningRate(earningRate))
+    }
+
+    fun printErrorMessage(message: String) {
+        println(message)
     }
 }
diff --git a/src/test/kotlin/lotto/model/AmountTest.kt b/src/test/kotlin/lotto/model/AmountTest.kt
index 9e94e8f06..5b8cb7c2c 100644
--- a/src/test/kotlin/lotto/model/AmountTest.kt
+++ b/src/test/kotlin/lotto/model/AmountTest.kt
@@ -1,26 +1,32 @@
 package lotto.model
 
+import lotto.domain.model.Amount
 import org.assertj.core.api.Assertions.assertThat
 import org.junit.jupiter.api.Test
-import org.junit.jupiter.api.assertThrows
 
 class AmountTest {
     @Test
-    fun `구입 금액이 0 이하인 경우 예외가 발생한다`() {
-        val exception =
-            assertThrows<IllegalArgumentException> {
-                Amount(-1)
-            }
-        assertThat(exception.message).isEqualTo("[ERROR] 금액은 양수이어야 합니다.")
+    fun `소지한 금액으로 로또 구매 가능한 개수를 반환한다`() {
+        val amount = Amount.valueOfOrNull(10000)
+        assertThat(amount).isNotNull
+        assertThat(amount?.getCount(1000)).isEqualTo(10)
     }
 
     @Test
-    fun `구입 금액이 로또 가격으로 나눌 수 없는 경우 예외가 발생한다`() {
-        val amount = Amount(9999)
-        val exception =
-            assertThrows<IllegalArgumentException> {
-                amount.moneySplit(Amount(1000))
-            }
-        assertThat(exception.message).isEqualTo("[ERROR] 로또 금액에 맞게 나눌 수 없습니다")
+    fun `소지한 금액이 음수면 null을 반환한다`() {
+        val amount = Amount.valueOfOrNull(-1)
+        assertThat(amount).isNull()
+    }
+
+    @Test
+    fun `구매 후 남은 금액을 가진 Amount 객체를 반환한다`() {
+        val amount = Amount.valueOfOrNull(10000)?.paymentOrNull(7000)
+        assertThat(amount?.money).isEqualTo(3000)
+    }
+
+    @Test
+    fun `소지한 금액보다 큰 금액을 지불하면 null을 반환한다`() {
+        val amount = Amount.valueOfOrNull(10000)?.paymentOrNull(17000)
+        assertThat(amount).isNull()
     }
 }
diff --git a/src/test/kotlin/lotto/model/LottoMachineTest.kt b/src/test/kotlin/lotto/model/LottoMachineTest.kt
deleted file mode 100644
index 0462c370e..000000000
--- a/src/test/kotlin/lotto/model/LottoMachineTest.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-package lotto.model
-
-import org.assertj.core.api.Assertions.assertThat
-import org.junit.jupiter.api.Test
-
-class TestShuffle : ShuffleStrategy {
-    override fun shuffle(list: List<Int>): List<Int> = listOf(45, 40, 35, 30, 25, 20)
-}
-
-class LottoMachineTest {
-    @Test
-    fun `입력된 금액에 맞게 로또를 발행한다`() {
-        val amount = Amount(10000)
-        val lottoMachine = LottoMachine(amount)
-        val lottoTickets = lottoMachine.publishLottoTickets(Amount(LOTTO_PRIZE))
-        assertThat(lottoTickets.size).isEqualTo(amount.money / LOTTO_PRIZE)
-    }
-
-    @Test
-    fun `로또 번호는 랜덤으로 섞인 리스트에서 6개를 뽑아서 지정된다`() {
-        val amount = Amount(1000)
-        val lottoMachine = LottoMachine(amount, TestShuffle())
-        val lottoTickets = lottoMachine.publishLottoTickets(Amount(LOTTO_PRIZE))
-        assertThat(lottoTickets[0].getNumbers().map { it.value }).isEqualTo(listOf(20, 25, 30, 35, 40, 45))
-    }
-
-    companion object {
-        const val LOTTO_PRIZE = 1000
-    }
-}
diff --git a/src/test/kotlin/lotto/model/LottoMatcherTest.kt b/src/test/kotlin/lotto/model/LottoMatcherTest.kt
deleted file mode 100644
index cee6e2c5a..000000000
--- a/src/test/kotlin/lotto/model/LottoMatcherTest.kt
+++ /dev/null
@@ -1,84 +0,0 @@
-package lotto.model
-
-import org.assertj.core.api.Assertions.assertThat
-import org.junit.jupiter.api.BeforeEach
-import org.junit.jupiter.api.Test
-import org.junit.jupiter.api.assertThrows
-import org.junit.jupiter.params.ParameterizedTest
-import org.junit.jupiter.params.provider.CsvSource
-
-class LottoMatcherTest {
-    private lateinit var winningLotto: Lotto
-    private lateinit var lottoMatcher: LottoMatcher
-
-    @BeforeEach
-    fun setup() {
-        val bonusNumber = LottoNumber(7)
-        winningLotto = Lotto(listOf(1, 2, 3, 4, 5, 6))
-        lottoMatcher = LottoMatcher(winningLotto, bonusNumber)
-    }
-
-    @Test
-    fun `당첨 번호와 보너스 볼은 중복될 수 없다`() {
-        val winningNumber = listOf(1, 2, 3, 4, 5, 6)
-        val winningLotto = Lotto(winningNumber)
-        val bonusNumber = LottoNumber(1)
-        val exception = assertThrows<IllegalArgumentException> { LottoMatcher(winningLotto, bonusNumber) }
-        assertThat(exception.message).isEqualTo("[ERROR] 당첨 번호와 보너스 볼은 중복될 수 없습니다.")
-    }
-
-    @Test
-    fun `당첨 번호와 일치하는 번호가 6개인 경우 1등이 된다`() {
-        val numbers = listOf(1, 2, 3, 4, 5, 6)
-        val publishedLotto = Lotto(numbers)
-        val rank = lottoMatcher.matchLotto(listOf(publishedLotto))
-        assertThat(rank[Rank.FIRST]).isEqualTo(1)
-    }
-
-    @Test
-    fun `당첨 번호와 일치하는 번호가 5개이고, 보너스 볼이 일치하는 경우 2등이 된다`() {
-        val numbers = listOf(1, 2, 3, 4, 5, 7)
-        val publishedLotto = Lotto(numbers)
-        val rank = lottoMatcher.matchLotto(listOf(publishedLotto))
-        assertThat(rank[Rank.SECOND]).isEqualTo(1)
-    }
-
-    @Test
-    fun `당첨 번호와 일치하는 번호가 5개인 경우 3등이 된다`() {
-        val numbers = listOf(1, 2, 3, 4, 5, 8)
-        val publishedLotto = Lotto(numbers)
-        val rank = lottoMatcher.matchLotto(listOf(publishedLotto))
-        assertThat(rank[Rank.THIRD]).isEqualTo(1)
-    }
-
-    @Test
-    fun `당첨 번호와 일치하는 번호가 4개인 경우 4등이 된다`() {
-        val numbers = listOf(1, 2, 3, 4, 8, 9)
-        val publishedLotto = Lotto(numbers)
-        val rank = lottoMatcher.matchLotto(listOf(publishedLotto))
-        assertThat(rank[Rank.FOURTH]).isEqualTo(1)
-    }
-
-    @Test
-    fun `당첨 번호와 일치하는 번호가 3개인 경우 5등이 된다`() {
-        val numbers = listOf(1, 2, 3, 8, 9, 10)
-        val publishedLotto = Lotto(numbers)
-        val rank = lottoMatcher.matchLotto(listOf(publishedLotto))
-        assertThat(rank[Rank.FIFTH]).isEqualTo(1)
-    }
-
-    @ParameterizedTest
-    @CsvSource(
-        "'1, 2, 8, 9, 10, 11'",
-        "'1, 8, 9, 10, 11, 12'",
-        "'8, 9, 10, 11, 12, 13'",
-    )
-    fun `당첨 번호와 일치하는 번호가 3개 미만인 경우 탈락된다`(numbers: String) {
-        val numbersList = numbers.split(",").map { it.trim().toInt() }
-        val publishedLotto = Lotto(numbersList)
-        val rank = lottoMatcher.matchLotto(listOf(publishedLotto))
-        assertThat(rank[Rank.MISS]).isEqualTo(1)
-    }
-
-    fun Lotto(numbers: List<Int>): Lotto = Lotto(LottoNumbers(numbers.map { number -> LottoNumber(number) }), Amount(1000))
-}
diff --git a/src/test/kotlin/lotto/model/LottoNumberTest.kt b/src/test/kotlin/lotto/model/LottoNumberTest.kt
index 569ce4029..7a18c014b 100644
--- a/src/test/kotlin/lotto/model/LottoNumberTest.kt
+++ b/src/test/kotlin/lotto/model/LottoNumberTest.kt
@@ -1,22 +1,19 @@
 package lotto.model
 
+import lotto.domain.model.LottoNumber
 import org.assertj.core.api.Assertions.assertThat
-import org.junit.jupiter.api.assertDoesNotThrow
+import org.junit.jupiter.api.Test
 import org.junit.jupiter.api.assertThrows
-import org.junit.jupiter.params.ParameterizedTest
-import org.junit.jupiter.params.provider.ValueSource
 
 class LottoNumberTest {
-    @ParameterizedTest
-    @ValueSource(ints = [1, 2, 3, 43, 44, 45])
-    fun `로또 번호는 1~45 사이여야 한다`(numbers: Int) {
-        assertDoesNotThrow { LottoNumber(numbers) }
+    @Test
+    fun `유효 범위를 벗어난 숫자로 LottoNumber 생성 시 null을 반환한다`() {
+        val lottoNumber = LottoNumber.valueOfOrNull(-1)
+        assertThat(lottoNumber).isNull()
     }
 
-    @ParameterizedTest
-    @ValueSource(ints = [-1, 0, 46, 47])
-    fun `로또 번호가 1~45 사이가 아닌 경우 예외가 발생한다`(numbers: Int) {
-        val exception = assertThrows<IllegalArgumentException> { LottoNumber(numbers) }
-        assertThat(exception.message).isEqualTo("[ERROR] 로또 번호는 1~45 사이여야 합니다.")
+    @Test
+    fun `유효 범위를 벗어난 숫자로 LottoNumber 생성 시 예외를 던진다`() {
+        assertThrows<IllegalArgumentException> { LottoNumber.valueOf(-1) }
     }
 }
diff --git a/src/test/kotlin/lotto/model/LottoNumbersTest.kt b/src/test/kotlin/lotto/model/LottoNumbersTest.kt
deleted file mode 100644
index f77d01e46..000000000
--- a/src/test/kotlin/lotto/model/LottoNumbersTest.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-package lotto.model
-
-import org.assertj.core.api.Assertions.assertThat
-import org.junit.jupiter.api.Test
-import org.junit.jupiter.api.assertThrows
-
-class LottoNumbersTest {
-    @Test
-    fun `로또 1매에는 6개의 로또 번호가 존재한다`() {
-        val lottoNumbers =
-            listOf(
-                LottoNumber(1),
-                LottoNumber(2),
-                LottoNumber(3),
-                LottoNumber(4),
-                LottoNumber(5),
-            )
-        val exception = assertThrows<IllegalArgumentException> { LottoNumbers(lottoNumbers) }
-        assertThat(exception.message).isEqualTo("[ERROR] 로또 번호는 6개여야 합니다.")
-    }
-
-    @Test
-    fun `로또 번호는 중복되지 않는다`() {
-        val lottoNumbers =
-            listOf(
-                LottoNumber(1),
-                LottoNumber(1),
-                LottoNumber(3),
-                LottoNumber(4),
-                LottoNumber(5),
-                LottoNumber(6),
-            )
-        val exception = assertThrows<IllegalArgumentException> { LottoNumbers(lottoNumbers) }
-        assertThat(exception.message).isEqualTo("[ERROR] 로또 번호는 중복될 수 없습니다.")
-    }
-}
diff --git a/src/test/kotlin/lotto/model/LottoTest.kt b/src/test/kotlin/lotto/model/LottoTest.kt
new file mode 100644
index 000000000..bdc678efb
--- /dev/null
+++ b/src/test/kotlin/lotto/model/LottoTest.kt
@@ -0,0 +1,52 @@
+package lotto.model
+
+import lotto.domain.model.Lotto
+import lotto.domain.model.LottoCreationResult
+import lotto.domain.model.LottoNumber
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertAll
+
+class LottoTest {
+    @Test
+    fun `로또 번호 개수가 6개가 아닐 경우 null을 반환한다`() {
+        val lotto = createLotto(listOf(1, 2, 3, 4, 5))
+        assertThat(lotto).isNull()
+    }
+
+    @Test
+    fun `로또 번호에 중복이 있을 경우 null을 반환한다`() {
+        val lotto = createLotto(listOf(1, 2, 3, 4, 5, 1))
+        assertThat(lotto).isNull()
+    }
+
+    @Test
+    fun `랜덤 생성 시 1부터 45 사이의 값으로 로또가 생성된다`() {
+        val lotto = Lotto.createRandom()
+        assertThat(lotto.numberList.all { it.value in 1..45 }).isTrue()
+    }
+
+    @Test
+    fun `로또는 내가 가진 번호중 몇개가 일치하는지 알 수 있다`() {
+        val lotto = createLotto(listOf(1, 2, 3, 4, 5, 6))
+        val lotto2 = createLotto(listOf(1, 2, 3, 4, 5, 7))
+        assertThat(lotto.countMatchNumbers(lotto2)).isEqualTo(5)
+    }
+
+    @Test
+    fun `로또는 특정 로또번호가 포함하는지 확인할 수 있다`() {
+        val lotto = createLotto(listOf(1, 2, 3, 4, 5, 6))
+        val lottoNumber = LottoNumber.valueOf(6)
+        val lottoNumber2 = LottoNumber.valueOf(7)
+        assertAll(
+            { assertThat(lotto.findNumber(lottoNumber)).isEqualTo(true) },
+            { assertThat(lotto.findNumber(lottoNumber2)).isEqualTo(false) },
+        )
+    }
+
+    private fun createLotto(numberList: List<Int>): Lotto =
+        when (val result = Lotto.valueOf(numberList.map { LottoNumber.valueOf(it) })) {
+            is LottoCreationResult.Success -> result.lotto
+            is LottoCreationResult.Failure -> throw IllegalArgumentException("잘못된 테스트 입력입니다.")
+        }
+}
diff --git a/src/test/kotlin/lotto/model/PrizeCalculatorTest.kt b/src/test/kotlin/lotto/model/PrizeCalculatorTest.kt
index bb0808b21..d823139a1 100644
--- a/src/test/kotlin/lotto/model/PrizeCalculatorTest.kt
+++ b/src/test/kotlin/lotto/model/PrizeCalculatorTest.kt
@@ -1,15 +1,25 @@
 package lotto.model
 
+import lotto.domain.model.Rank
+import lotto.domain.service.RankCalculator
 import org.assertj.core.api.Assertions.assertThat
 import org.junit.jupiter.api.Test
 
 class PrizeCalculatorTest {
     @Test
-    fun `총 수익을 입력 금액으로 나누어 수익률을 계산한다`() {
-        val money = 1000
-        val result = mapOf(Rank.FIFTH to 1)
-        val prizeCalculator = PrizeCalculator()
-        val earningRate = prizeCalculator.calculateEarningRate(money, result)
-        assertThat(earningRate).isEqualTo((Rank.FIFTH.winningMoney / 1000).toDouble())
+    fun `순위 리스트에 따라 총 당첨금을 계산한다`() {
+        val rankCalculator = RankCalculator()
+        val winningList = mapOf(Rank.FIRST to 1, Rank.SECOND to 1)
+        val totalPrize = Rank.FIRST.winningMoney + Rank.SECOND.winningMoney
+        assertThat(rankCalculator.earningMoney(winningList)).isEqualTo(totalPrize)
+    }
+
+    @Test
+    fun `당첨금과 투자 금액으로 수익률을 계산한다`() {
+        val rankCalculator = RankCalculator()
+        val winningList = listOf(Rank.FIRST, Rank.SECOND)
+        val totalPrize = Rank.FIRST.winningMoney + Rank.SECOND.winningMoney
+        assertThat(rankCalculator.calculateEarningRate(2000, totalPrize))
+            .isEqualTo(totalPrize / 2000.0)
     }
 }
diff --git a/src/test/kotlin/lotto/model/WinningListMakerTest.kt b/src/test/kotlin/lotto/model/WinningListMakerTest.kt
new file mode 100644
index 000000000..04c892e12
--- /dev/null
+++ b/src/test/kotlin/lotto/model/WinningListMakerTest.kt
@@ -0,0 +1,36 @@
+package lotto.model
+
+import lotto.domain.model.Lotto
+import lotto.domain.model.LottoCreationResult
+import lotto.domain.model.LottoNumber
+import lotto.domain.model.Rank
+import lotto.domain.model.WinningLotto
+import lotto.domain.service.WinningListMaker
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+
+class WinningListMakerTest {
+    @Test
+    fun `로또 리스트와 당첨 로또를 비교하여 순위 리스트를 반환한다`() {
+        val lottoList = listOf(createLotto(listOf(1, 2, 3, 4, 5, 6)), createLotto(listOf(1, 2, 3, 4, 5, 7)))
+        val winningLotto =
+            WinningLotto.valueOfOrNull(
+                createLottoNumbers(listOf(1, 2, 3, 4, 5, 6)),
+                LottoNumber.valueOf(7),
+            ) ?: throw IllegalStateException("WinningLotto 생성 실패")
+
+        val winningListMaker = WinningListMaker(winningLotto)
+        val result = winningListMaker.makeWinningList(lottoList)
+
+        assertThat(result[Rank.FIRST]).isEqualTo(1)
+        assertThat(result[Rank.SECOND]).isEqualTo(1)
+    }
+
+    private fun createLotto(numberList: List<Int>): Lotto =
+        when (val result = Lotto.valueOf(numberList.map { LottoNumber.valueOf(it) })) {
+            is LottoCreationResult.Success -> result.lotto
+            is LottoCreationResult.Failure -> throw IllegalArgumentException("Lotto 생성 실패")
+        }
+
+    private fun createLottoNumbers(numberList: List<Int>): List<LottoNumber> = numberList.map { LottoNumber.valueOf(it) }
+}


Reviews:
user: login: laco-dev
body: 2단계 고생 많으셨습니다.

논리적인 오류를 인지하는데 있어 고민을 많이 하신 흔적이 보입니다.
다만, 논리적인 오류에 대한 개념을 잘못 잡고 계신 부분이 문제가 된 것 같습니다.

크루들과 논리적인 오류는 무엇인가? 에 대한 주제부터 먼저 토론을 해봐야 할 것 같아요.
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-02-25T15:08:34Z
user: login: Leeyerin0210
state: COMMENTED
author_association: NONE
submitted_at: 2025-02-26T06:52:28Z
user: login: Leeyerin0210
state: COMMENTED
author_association: NONE
submitted_at: 2025-02-26T06:53:06Z
user: login: Leeyerin0210
state: COMMENTED
author_association: NONE
submitted_at: 2025-02-26T06:53:19Z
user: login: Leeyerin0210
state: COMMENTED
author_association: NONE
submitted_at: 2025-02-26T06:54:17Z
user: login: Leeyerin0210
state: COMMENTED
author_association: NONE
submitted_at: 2025-02-26T06:56:50Z
user: login: Leeyerin0210
state: COMMENTED
author_association: NONE
submitted_at: 2025-02-26T06:58:33Z
user: login: Leeyerin0210
state: COMMENTED
author_association: NONE
submitted_at: 2025-02-26T10:13:17Z
user: login: Leeyerin0210
state: COMMENTED
author_association: NONE
submitted_at: 2025-02-26T10:14:41Z
user: login: Leeyerin0210
state: COMMENTED
author_association: NONE
submitted_at: 2025-02-26T10:23:38Z
user: login: laco-dev
state: COMMENTED
author_association: NONE
submitted_at: 2025-02-26T13:43:13Z
user: login: laco-dev
body: > 예외 처리를 통해 에러가 발생하지 않도록 한다.

일반적으로 try-catch를 사용하지 않는 말 때문에 문제가 시작된 것으로 보이지만...
좋은 코드는 정말  이렇게 짜야 한다고..? 라는 어색함이 생기기를 바라고 있습니다 😄 

예외 처리 관련해서는 더이상 개선하지는 마시고, 나중에 더 학습이 되신 후에 지금 코드를 돌아보시는 방법이 좋을 것 같아요.
나중에 자신의 코드를 돌아볼 때 느끼는 점이 훨씬 클 것 같다?

지금은 로또를 진행하기 위한 어색한 흐름을 해결하고 정리하는 것을 챙겨보시죠.
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-02-26T14:41:47Z
user: login: laco-dev
body: 시아 고생 많으셨습니다.

전부 갈아엎고 다시 시도한 것만으로도 우선 칭찬드리고 싶습니다.
TDD 관점에서 리뷰를 드렸으며 객체지향적인 사고와 API 설계 관점에서 다시 처음부터 의견 남겼습니다.

객체가 스스로 해야할 역할을 팩토리 메서드나 다른 클래스에 만들어 자율적인 객체와는 거리가 더 멀어진 것 같습니다.

더 성장하시고 싶어 하신 모습에 따라 피드백이 다소 쎄게 들어갔으니 양해 부탁해요. 😢 
할 수 있는 만큼만 반영 하시고 6시 전에 제출하려고 하다말고 올리는 것보다는 차라리 전부 반영하고 나중에 다시 올려주시는게 나을 것 같아요.
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-03-03T02:24:46Z
user: login: Leeyerin0210
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-06T00:53:36Z
user: login: Leeyerin0210
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-06T03:37:18Z
user: login: Leeyerin0210
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-06T03:41:44Z
user: login: Leeyerin0210
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-06T03:50:12Z
user: login: Leeyerin0210
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-06T03:58:05Z
user: login: Leeyerin0210
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-06T04:04:07Z
user: login: Leeyerin0210
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-09T08:31:50Z
user: login: Leeyerin0210
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-09T08:32:59Z
user: login: Leeyerin0210
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-13T02:17:44Z
user: login: Leeyerin0210
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-13T02:21:19Z
user: login: Leeyerin0210
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-13T02:25:29Z
user: login: Leeyerin0210
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-13T02:27:45Z
user: login: Leeyerin0210
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-13T02:38:27Z
user: login: Leeyerin0210
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-13T02:40:03Z
user: login: Leeyerin0210
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-13T02:42:03Z
user: login: Leeyerin0210
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-13T04:06:36Z
user: login: Leeyerin0210
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-13T04:06:46Z
user: login: Leeyerin0210
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-13T04:07:27Z
user: login: Leeyerin0210
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-13T04:12:06Z
user: login: Leeyerin0210
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-13T04:17:59Z
user: login: laco-dev
body: 로또 미션 마감은 지난주에 종료가 되어서, 추가적인 리뷰는 따로 남기지 않았어요.
새로 시도하실 때 드렸던 유의점을 인지하고 하셨기 때문에 저는 고생하셨다고만 남겨드리겠습니다!

앞으로의 우테코 기간도 잘 보내셨으면 좋겠습니다. 
state: APPROVED
author_association: NONE
submitted_at: 2025-03-13T10:49:55Z
user: login: laco-dev
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-13T10:52:23Z

Review comments:
diff_hunk: @@ -2,28 +2,27 @@ package lotto.model
 
 enum class Rank(
     val countOfMatch: Int,
+    val bonusRequired: Boolean,
     val winningMoney: Int,
+    val printSequence: Int?,
path: src/main/kotlin/lotto/model/Rank.kt
user: login: laco-dev
body: > 출력 코드를 개선하기 위해 enum에 출력 관련 정보들을 넣어뒀는데, 이러면 뷰와 도메인이 너무 강하게 결합된 것일까요? 처음엔 아에 enum을 순회하면서 출력하도록 했다가, 너무 결합된 안 좋은 코드 같아서, 다시 result을 가져와서 출력하도록 했습니다.

넵, 결합된 것 같습니다.
물론 서버에서 출력 순서를 결정하는 경우도 있지만 프레젠테이션 레이어는 도메인과 분리를 하는게 일반적이니까요.

사실 그렇게 복잡하게 생각할 필요 없이 단순하게 해도 되지만,
```kotlin
listOf(Rank.First, Rank.Second, ...).forEach { ... }
```
혹은 서버에서 순서를 내려주는 것처럼 만들어 볼 수도 있지만,
```kotlin
val rankResulsts: List<Rank> = ...
```
지금 단계에서는 그렇게 복잡하게 잡지 말고 UI랑 도메인 분리하는 것만 먼저 연습해봅시다.

별개로 Rank가 enum으로 사전에 모두 정의되어 있어서 UI 모델을 만드는 것도 방법입니다.
다만 지금 다룰 주제는 아니기 때문에 반영은 하지 마시고 printSequence만 도메인에서 떨어트려보면 좋을 것 같아요.
created_at: 2025-02-25T14:06:39Z
author_association: NONE
diff_hunk: @@ -17,36 +17,65 @@ class LottoController(
     fun run() {
         val amount = getAmount()
         val lottoMachine = LottoMachine(amount)
-        val publishedLotto = publishLotto(lottoMachine)
-        val winningLotto = getWinningLotto()
-        val bonusNumber = getBonusNumber()
-        val lottoMatcher = LottoMatcher(winningLotto, bonusNumber)
-        showEarningRate(amount, lottoMatcher, publishedLotto)
+        val publishedLotto = validatePublishLottoList(lottoMachine) + autoPublishLotto(lottoMachine)
+        val winningLotto = validateWinningLotto(getWinningLotto(), getBonusNumber())
+        val prizeCalculator = PrizeCalculator(winningLotto, publishedLotto, amount)
+        showEarningRate(prizeCalculator)
     }
 
     private fun getAmount(): Amount = Amount(inputView.getMoney())
 
-    private fun publishLotto(lottoMachine: LottoMachine): List<Lotto> {
-        val publishedLotto = lottoMachine.publishLottoTickets(Amount(LOTTO_PRIZE))
+    private fun validateWinningLotto(
+        number: LottoNumbers?,
+        bonus: LottoNumber?,
+    ): WinningLotto {
+        if (number == null || bonus == null) {
+            outputView.inputWinningError()
+            return validateWinningLotto(getWinningLotto(), getBonusNumber())
+        }
+        return WinningLotto(number, bonus)
+    }
path: src/main/kotlin/lotto/controller/LottoController.kt
user: login: laco-dev
body: > 요구사항은 에러가 생겼을 때 null을 리턴하라고 했으나, null을 사용하면 후속 코드가 전부 꼬이는 것 같습니다. 그래서 잘못된 입력을 받았을 때 객체에서 null을 반환하고, 컨트롤러가 null을 확인 하면 다시 입력 받도록 하였는데, 이게 맞는 방법인지 잘 모르겠습니다. null을 받았을때 null 을 계속 가져갈 수 있도록 코드를 짜는 게 좋을까요, 아니면 null은 그 자리에서 다시 입력받아서 해결시키는게 나은 방법일까요?

모두 적절한 방법이라고 생각은 하는데요.
파라미터로 입력받는 구조가 조금 어색하게 느껴집니다.
결국 하고싶은 역할은 당첨 로또를 만들어내는 것이라면 그에 맞게 명시를 해주면 되지 않을까요?

요구사항에서 제시하는 것은 예외를 발생시키는 대신, 코드를 안전하게 작성할 수 있도록 null을 활용하라는 취지인데요.
논리적인 오류라면, 당연히 예외를 발생시키겠지만 그렇지 않은 경우는 유연하게 작성을 합니다.
`String.toIntOrNull()` `String.toInt()` 이런 차이가 대표적인 예시입니다.

```kotlin
// example
private fun getWinningLotto(): WinningLotto { 
   val bonusNumber: LottoNumber? = ...
   val winningLottoNumbers: LottoNumbers? = ...
   if (...) { ... }
   return ...
}
```
created_at: 2025-02-25T14:15:16Z
author_association: NONE
diff_hunk: @@ -17,36 +17,65 @@ class LottoController(
     fun run() {
         val amount = getAmount()
         val lottoMachine = LottoMachine(amount)
-        val publishedLotto = publishLotto(lottoMachine)
-        val winningLotto = getWinningLotto()
-        val bonusNumber = getBonusNumber()
-        val lottoMatcher = LottoMatcher(winningLotto, bonusNumber)
-        showEarningRate(amount, lottoMatcher, publishedLotto)
+        val publishedLotto = validatePublishLottoList(lottoMachine) + autoPublishLotto(lottoMachine)
+        val winningLotto = validateWinningLotto(getWinningLotto(), getBonusNumber())
+        val prizeCalculator = PrizeCalculator(winningLotto, publishedLotto, amount)
+        showEarningRate(prizeCalculator)
     }
 
     private fun getAmount(): Amount = Amount(inputView.getMoney())
 
-    private fun publishLotto(lottoMachine: LottoMachine): List<Lotto> {
-        val publishedLotto = lottoMachine.publishLottoTickets(Amount(LOTTO_PRIZE))
+    private fun validateWinningLotto(
+        number: LottoNumbers?,
+        bonus: LottoNumber?,
+    ): WinningLotto {
+        if (number == null || bonus == null) {
+            outputView.inputWinningError()
+            return validateWinningLotto(getWinningLotto(), getBonusNumber())
+        }
+        return WinningLotto(number, bonus)
+    }
+
+    private fun validatePublishLotto(lottoMachine: LottoMachine): Lotto {
+        var lotto = lottoMachine.publishManualLotto(inputView.getManualLotto())
+        while (lotto == null) {
+            outputView.inputLottoError()
+            lotto = lottoMachine.publishManualLotto(inputView.getManualLotto())
+        }
+        return lotto
+    }
path: src/main/kotlin/lotto/controller/LottoController.kt
user: login: laco-dev
body: > 예외 발생 시 다시 입력받도록 해서 예외 처리 테스트가 불가능한데, 이런 경우에는 출력문과 직접 비교하여 테스트하면 되는 것일까요?

무엇을 테스트 하고 싶은 것인지 명확하지 않은 것 같습니다.
정말 논리적인 오류 상황이라면 예외를 테스트 하는게 맞아요, 그렇지 않은 상황이라면 그에 맞는 테스트를 작성하면 됩니다.

한가지 예시를 들어볼게요. 아래 1번과 2번 상황의 차이가 보이나요?
> 로또를 구매하기 위해 1,500원과 로또 번호 한 세트를 전달했다.
> 돈이 1,500원입니다. 프로그램을 종료합니다.

> 로또를 구매하기 위해 1,500원과 로또 번호 한 세트를 전달했다.
> 로또 1장을 구매했습니다. 거스름돈은 500원입니다.
created_at: 2025-02-25T14:21:32Z
author_association: NONE
diff_hunk: @@ -17,36 +17,65 @@ class LottoController(
     fun run() {
         val amount = getAmount()
         val lottoMachine = LottoMachine(amount)
-        val publishedLotto = publishLotto(lottoMachine)
-        val winningLotto = getWinningLotto()
-        val bonusNumber = getBonusNumber()
-        val lottoMatcher = LottoMatcher(winningLotto, bonusNumber)
-        showEarningRate(amount, lottoMatcher, publishedLotto)
+        val publishedLotto = validatePublishLottoList(lottoMachine) + autoPublishLotto(lottoMachine)
+        val winningLotto = validateWinningLotto(getWinningLotto(), getBonusNumber())
+        val prizeCalculator = PrizeCalculator(winningLotto, publishedLotto, amount)
+        showEarningRate(prizeCalculator)
     }
 
     private fun getAmount(): Amount = Amount(inputView.getMoney())
 
-    private fun publishLotto(lottoMachine: LottoMachine): List<Lotto> {
-        val publishedLotto = lottoMachine.publishLottoTickets(Amount(LOTTO_PRIZE))
+    private fun validateWinningLotto(
+        number: LottoNumbers?,
+        bonus: LottoNumber?,
+    ): WinningLotto {
+        if (number == null || bonus == null) {
+            outputView.inputWinningError()
+            return validateWinningLotto(getWinningLotto(), getBonusNumber())
+        }
+        return WinningLotto(number, bonus)
+    }
+
+    private fun validatePublishLotto(lottoMachine: LottoMachine): Lotto {
+        var lotto = lottoMachine.publishManualLotto(inputView.getManualLotto())
+        while (lotto == null) {
+            outputView.inputLottoError()
+            lotto = lottoMachine.publishManualLotto(inputView.getManualLotto())
+        }
+        return lotto
+    }
path: src/main/kotlin/lotto/controller/LottoController.kt
user: login: laco-dev
body: 하나는 재귀로 하고, 하나는 반복문을 한게 의도인 것 같은데요!
코드 작성할 때 가장 중요한 요소 중 하나는 일관성입니다.

다른 사람이 이어서 개발을 한다고 하면 뭐로 해야하지? 라는 착각에 빠지기 때문에 여러개로 구현이 나뉘는 것보다 하나의 일관된 흐름을 만드는것이 좋습니다.
created_at: 2025-02-25T14:23:04Z
author_association: NONE
diff_hunk: @@ -1,21 +1,25 @@
 package lotto.model
 
 data class Amount(
-    val money: Int,
+    var money: Int,
 ) {
     init {
-        require(money > 0) { MONEY_UNDER_ZERO }
+        require(money >= 0) { MONEY_UNDER_ZERO }
+    }
+
+    fun payMoney(inputMoney: Amount) {
+        require(inputMoney.money <= money) { MONEY_OVER_INPUT }
+        money -= inputMoney.money
path: src/main/kotlin/lotto/model/Amount.kt
user: login: laco-dev
body: 이런데서도 논리적인 오류를 예시로 들 수 있는데요.
예를 들어 돈이 마이너스로 내려가면 프로그램이 종료된다와
돈은 최소 Amount.Zero 까지만 내려간다. 라는 차이를 들 수 있겠네요
created_at: 2025-02-25T14:25:35Z
author_association: NONE
diff_hunk: @@ -11,29 +11,41 @@ class NormalShuffle : ShuffleStrategy {
 }
 
 class LottoMachine(
-    private val amount: Amount,
+    var amount: Amount,
     private val shuffle: ShuffleStrategy = NormalShuffle(),
 ) {
     private val numbers: List<Int> = (LottoNumber.MIN_VALUE..LottoNumber.MAX_VALUE).toList()
 
-    fun publishLottoTickets(lottoPrize: Amount): List<Lotto> {
+    fun useMoney(inputMoney: Amount): Boolean {
+        if (inputMoney.money > amount.money) return false
+        amount.payMoney(inputMoney)
+        return true
+    }
path: src/main/kotlin/lotto/model/LottoMachine.kt
user: login: laco-dev
body: 돈을 넣는다는 기능이 별도로 빠진 이점을 잘 모르겠어요!
로또 기계는 왜 스스로 가진 돈을 소모해버리는 건지도 잘 와닿지가 않아요. 🤔 

이런 느낌이려나요..?
> 5,000원을 준비한다.
> 수동 로또 3개를 구매한다.
> 남은 돈은 2,000원이다.
> 자동 로또를 2개 구매한다.
> 남은 돈이 없다!

그런데 로또 기계에 돈을 결합해서 상태를 관리할 필요가 있을까요..?
created_at: 2025-02-25T14:33:42Z
author_association: NONE
diff_hunk: @@ -1,21 +1,25 @@
 package lotto.model
 
 data class Amount(
-    val money: Int,
+    var money: Int,
path: src/main/kotlin/lotto/model/Amount.kt
user: login: laco-dev
body: 이 코드의 문제점을 알려주세요
```kotlin
val amount = Amount(5000)
amount.money = -5000
```
created_at: 2025-02-25T14:46:34Z
author_association: NONE
diff_hunk: @@ -4,15 +4,16 @@ package lotto.model
 value class LottoNumber(
     val value: Int,
 ) {
-    init {
-        require(value in MIN_VALUE..MAX_VALUE) {
-            RANGE_ERROR_MESSAGE
-        }
-    }
-
     companion object {
         const val MIN_VALUE = 1
         const val MAX_VALUE = 45
         private const val RANGE_ERROR_MESSAGE = "[ERROR] 로또 번호는 1~45 사이여야 합니다."
+
+        fun create(value: Int): LottoNumber? {
+            if (value !in MIN_VALUE..MAX_VALUE) {
+                return null
+            }
+            return LottoNumber(value)
+        }
path: src/main/kotlin/lotto/model/LottoNumber.kt
user: login: laco-dev
body: 극단적으로 모든 케이스를 null로 잡으려고 해서 끝도없이 늘어지는 것 같습니다.

LottoNumber의 정체성인 1부터 45까지다 라는 논리적인 오류는 유지하면서,
프로그램을 사용하는데 있어 유연하게 구현을 하는게 맞지 않을까요?

지금 코드에서는 이렇게 호출해도 문제가 없는 코드입니다. LottoNumber(-999)

created_at: 2025-02-25T14:58:26Z
author_association: NONE
diff_hunk: @@ -1,18 +1,29 @@
 package lotto.model
 
-class PrizeCalculator {
-    fun calculateInputMoney(lottoList: List<Lotto>): Int {
-        val allAmount = 0
-        lottoList.forEach {
-            allAmount + it.prize.money
+class PrizeCalculator(
+    val winningLotto: WinningLotto,
+    val publishedLotto: List<Lotto>,
+    val amount: Amount,
+) {
+    val result: Map<Rank, Int>
path: src/main/kotlin/lotto/model/PrizeCalculator.kt
user: login: laco-dev
body: 이 result에 담긴 value가 무엇인지 사용하는 쪽에서 알 수 있을까요?
클래스 이름으로 유추해보면 value는 당첨금 합계구나? 라는 착각을 하지는 않을까요?

객체에게 필요한 기능을 메시지로 나타내면 무엇일까요?
그리고 그것을 메소드로 제공한다면 어떻게 해야 할까요?
created_at: 2025-02-25T15:02:36Z
author_association: NONE
diff_hunk: @@ -11,29 +11,41 @@ class NormalShuffle : ShuffleStrategy {
 }
 
 class LottoMachine(
-    private val amount: Amount,
+    var amount: Amount,
     private val shuffle: ShuffleStrategy = NormalShuffle(),
 ) {
     private val numbers: List<Int> = (LottoNumber.MIN_VALUE..LottoNumber.MAX_VALUE).toList()
 
-    fun publishLottoTickets(lottoPrize: Amount): List<Lotto> {
+    fun useMoney(inputMoney: Amount): Boolean {
+        if (inputMoney.money > amount.money) return false
+        amount.payMoney(inputMoney)
+        return true
+    }
+
+    fun publishManualLotto(numberList: List<Int>): Lotto? {
+        var lottoNumbers: LottoNumbers? = LottoNumbers.create(numberList.mapNotNull { it -> LottoNumber.create(it) })
+        if (lottoNumbers == null) return null
+        return Lotto(lottoNumbers)
+    }
path: src/main/kotlin/lotto/model/LottoMachine.kt
user: login: laco-dev
body: 이게 null이 반환된다는게 뭐때문에 null이 반환되었는지 판단할 수 있는건가요..?
숫자가 이상하게 들어오면 논리적으로 명확한 오류 상황에 직면하는데요.

예를들어 개발자가 실수로 숫자 5개만 넣도록 구현을 잘못했는데 listOf(1, 1, 2, 3, 4)
계속해서 null이 들어와서 오류 리포팅이 안되고 있는 상황이 정상적인 상황으로 봐야할까요?
created_at: 2025-02-25T15:07:11Z
author_association: NONE
diff_hunk: @@ -2,28 +2,27 @@ package lotto.model
 
 enum class Rank(
     val countOfMatch: Int,
+    val bonusRequired: Boolean,
     val winningMoney: Int,
+    val printSequence: Int?,
path: src/main/kotlin/lotto/model/Rank.kt
user: login: Leeyerin0210
body: [1db3c57](https://github.com/woowacourse/kotlin-lotto/pull/135/commits/1db3c5770bb025260e732f33ae340d681d430334)

컨트롤러에서 정렬과 일부 제거 시키고 뷰에 옮겨서 뷰는 단순히 출력만 할 수 있게 만들어 보았습니다!
created_at: 2025-02-26T06:52:28Z
author_association: NONE
diff_hunk: @@ -17,36 +17,65 @@ class LottoController(
     fun run() {
         val amount = getAmount()
         val lottoMachine = LottoMachine(amount)
-        val publishedLotto = publishLotto(lottoMachine)
-        val winningLotto = getWinningLotto()
-        val bonusNumber = getBonusNumber()
-        val lottoMatcher = LottoMatcher(winningLotto, bonusNumber)
-        showEarningRate(amount, lottoMatcher, publishedLotto)
+        val publishedLotto = validatePublishLottoList(lottoMachine) + autoPublishLotto(lottoMachine)
+        val winningLotto = validateWinningLotto(getWinningLotto(), getBonusNumber())
+        val prizeCalculator = PrizeCalculator(winningLotto, publishedLotto, amount)
+        showEarningRate(prizeCalculator)
     }
 
     private fun getAmount(): Amount = Amount(inputView.getMoney())
 
-    private fun publishLotto(lottoMachine: LottoMachine): List<Lotto> {
-        val publishedLotto = lottoMachine.publishLottoTickets(Amount(LOTTO_PRIZE))
+    private fun validateWinningLotto(
+        number: LottoNumbers?,
+        bonus: LottoNumber?,
+    ): WinningLotto {
+        if (number == null || bonus == null) {
+            outputView.inputWinningError()
+            return validateWinningLotto(getWinningLotto(), getBonusNumber())
+        }
+        return WinningLotto(number, bonus)
+    }
path: src/main/kotlin/lotto/controller/LottoController.kt
user: login: Leeyerin0210
body: [4485eac](https://github.com/woowacourse/kotlin-lotto/pull/135/commits/4485eacccffeb80aced4fbc0ce4441da0abdd723)
sealed 클래스를 활용해 작성해보았습니다
created_at: 2025-02-26T06:53:06Z
author_association: NONE
diff_hunk: @@ -17,36 +17,65 @@ class LottoController(
     fun run() {
         val amount = getAmount()
         val lottoMachine = LottoMachine(amount)
-        val publishedLotto = publishLotto(lottoMachine)
-        val winningLotto = getWinningLotto()
-        val bonusNumber = getBonusNumber()
-        val lottoMatcher = LottoMatcher(winningLotto, bonusNumber)
-        showEarningRate(amount, lottoMatcher, publishedLotto)
+        val publishedLotto = validatePublishLottoList(lottoMachine) + autoPublishLotto(lottoMachine)
+        val winningLotto = validateWinningLotto(getWinningLotto(), getBonusNumber())
+        val prizeCalculator = PrizeCalculator(winningLotto, publishedLotto, amount)
+        showEarningRate(prizeCalculator)
     }
 
     private fun getAmount(): Amount = Amount(inputView.getMoney())
 
-    private fun publishLotto(lottoMachine: LottoMachine): List<Lotto> {
-        val publishedLotto = lottoMachine.publishLottoTickets(Amount(LOTTO_PRIZE))
+    private fun validateWinningLotto(
+        number: LottoNumbers?,
+        bonus: LottoNumber?,
+    ): WinningLotto {
+        if (number == null || bonus == null) {
+            outputView.inputWinningError()
+            return validateWinningLotto(getWinningLotto(), getBonusNumber())
+        }
+        return WinningLotto(number, bonus)
+    }
+
+    private fun validatePublishLotto(lottoMachine: LottoMachine): Lotto {
+        var lotto = lottoMachine.publishManualLotto(inputView.getManualLotto())
+        while (lotto == null) {
+            outputView.inputLottoError()
+            lotto = lottoMachine.publishManualLotto(inputView.getManualLotto())
+        }
+        return lotto
+    }
path: src/main/kotlin/lotto/controller/LottoController.kt
user: login: Leeyerin0210
body: [4485eac](https://github.com/woowacourse/kotlin-lotto/pull/135/commits/4485eacccffeb80aced4fbc0ce4441da0abdd723)
전부 재귀로 통일시켰습니다!
created_at: 2025-02-26T06:53:19Z
author_association: NONE
diff_hunk: @@ -11,29 +11,41 @@ class NormalShuffle : ShuffleStrategy {
 }
 
 class LottoMachine(
-    private val amount: Amount,
+    var amount: Amount,
     private val shuffle: ShuffleStrategy = NormalShuffle(),
 ) {
     private val numbers: List<Int> = (LottoNumber.MIN_VALUE..LottoNumber.MAX_VALUE).toList()
 
-    fun publishLottoTickets(lottoPrize: Amount): List<Lotto> {
+    fun useMoney(inputMoney: Amount): Boolean {
+        if (inputMoney.money > amount.money) return false
+        amount.payMoney(inputMoney)
+        return true
+    }
path: src/main/kotlin/lotto/model/LottoMachine.kt
user: login: Leeyerin0210
body: [4485eac](https://github.com/woowacourse/kotlin-lotto/pull/135/commits/4485eacccffeb80aced4fbc0ce4441da0abdd723)
굳이 머신이 가지고 있는 것 보다 컨트롤러가 가지고 있는 돈을 직접 소모하는게 나을 것 같아서 수정했습니다!
created_at: 2025-02-26T06:54:17Z
author_association: NONE
diff_hunk: @@ -1,21 +1,25 @@
 package lotto.model
 
 data class Amount(
-    val money: Int,
+    var money: Int,
path: src/main/kotlin/lotto/model/Amount.kt
user: login: Leeyerin0210
body: [4485eac](https://github.com/woowacourse/kotlin-lotto/pull/135/commits/4485eacccffeb80aced4fbc0ce4441da0abdd723)
직접 money에 접근해서 수정해버리면, 값이 마음대로 변경되는 것도 문제지만, init 안에 있는 유효성 검사가 실행되지 않아서 문제가 생길 수 있겠네요! 금액을 뺀 값을 가진 새로운 amount 객체를 리턴하는 방법으로 수정해 보았습니다!
created_at: 2025-02-26T06:56:50Z
author_association: NONE
diff_hunk: @@ -11,29 +11,41 @@ class NormalShuffle : ShuffleStrategy {
 }
 
 class LottoMachine(
-    private val amount: Amount,
+    var amount: Amount,
     private val shuffle: ShuffleStrategy = NormalShuffle(),
 ) {
     private val numbers: List<Int> = (LottoNumber.MIN_VALUE..LottoNumber.MAX_VALUE).toList()
 
-    fun publishLottoTickets(lottoPrize: Amount): List<Lotto> {
+    fun useMoney(inputMoney: Amount): Boolean {
+        if (inputMoney.money > amount.money) return false
+        amount.payMoney(inputMoney)
+        return true
+    }
+
+    fun publishManualLotto(numberList: List<Int>): Lotto? {
+        var lottoNumbers: LottoNumbers? = LottoNumbers.create(numberList.mapNotNull { it -> LottoNumber.create(it) })
+        if (lottoNumbers == null) return null
+        return Lotto(lottoNumbers)
+    }
path: src/main/kotlin/lotto/model/LottoMachine.kt
user: login: Leeyerin0210
body: [4485eac](https://github.com/woowacourse/kotlin-lotto/pull/135/commits/4485eacccffeb80aced4fbc0ce4441da0abdd723)
객체 init 안의 require를 유지하고, 외부에서 생성할때 유효성을 한번 돌림으로써 유저의 잘못과 논리적인 오류를 따로 볼 수 있도록 변경했습니다
created_at: 2025-02-26T06:58:33Z
author_association: NONE
diff_hunk: @@ -17,36 +17,65 @@ class LottoController(
     fun run() {
         val amount = getAmount()
         val lottoMachine = LottoMachine(amount)
-        val publishedLotto = publishLotto(lottoMachine)
-        val winningLotto = getWinningLotto()
-        val bonusNumber = getBonusNumber()
-        val lottoMatcher = LottoMatcher(winningLotto, bonusNumber)
-        showEarningRate(amount, lottoMatcher, publishedLotto)
+        val publishedLotto = validatePublishLottoList(lottoMachine) + autoPublishLotto(lottoMachine)
+        val winningLotto = validateWinningLotto(getWinningLotto(), getBonusNumber())
+        val prizeCalculator = PrizeCalculator(winningLotto, publishedLotto, amount)
+        showEarningRate(prizeCalculator)
     }
 
     private fun getAmount(): Amount = Amount(inputView.getMoney())
 
-    private fun publishLotto(lottoMachine: LottoMachine): List<Lotto> {
-        val publishedLotto = lottoMachine.publishLottoTickets(Amount(LOTTO_PRIZE))
+    private fun validateWinningLotto(
+        number: LottoNumbers?,
+        bonus: LottoNumber?,
+    ): WinningLotto {
+        if (number == null || bonus == null) {
+            outputView.inputWinningError()
+            return validateWinningLotto(getWinningLotto(), getBonusNumber())
+        }
+        return WinningLotto(number, bonus)
+    }
+
+    private fun validatePublishLotto(lottoMachine: LottoMachine): Lotto {
+        var lotto = lottoMachine.publishManualLotto(inputView.getManualLotto())
+        while (lotto == null) {
+            outputView.inputLottoError()
+            lotto = lottoMachine.publishManualLotto(inputView.getManualLotto())
+        }
+        return lotto
+    }
path: src/main/kotlin/lotto/controller/LottoController.kt
user: login: Leeyerin0210
body: [63bb963](https://github.com/woowacourse/kotlin-lotto/pull/135/commits/63bb96374065b54a56a74ebd828849cc57e4132f)
1번은 논리적으로 충분히 로또 한 장을 살 수 있음에도 불구하고 정상적으로 작동하지 않는것이 문제인 것 같습니다..! 정말로 논리적인 오류 상황이 아니면 가급적 예외를 내보내는건 주의해야겠네요!
created_at: 2025-02-26T10:13:17Z
author_association: NONE
diff_hunk: @@ -4,15 +4,16 @@ package lotto.model
 value class LottoNumber(
     val value: Int,
 ) {
-    init {
-        require(value in MIN_VALUE..MAX_VALUE) {
-            RANGE_ERROR_MESSAGE
-        }
-    }
-
     companion object {
         const val MIN_VALUE = 1
         const val MAX_VALUE = 45
         private const val RANGE_ERROR_MESSAGE = "[ERROR] 로또 번호는 1~45 사이여야 합니다."
+
+        fun create(value: Int): LottoNumber? {
+            if (value !in MIN_VALUE..MAX_VALUE) {
+                return null
+            }
+            return LottoNumber(value)
+        }
path: src/main/kotlin/lotto/model/LottoNumber.kt
user: login: Leeyerin0210
body: [4485eac](https://github.com/woowacourse/kotlin-lotto/pull/135/commits/4485eacccffeb80aced4fbc0ce4441da0abdd723)
객체의 정채성은 유지하고, 객체를 생성하기 전에 유효성 검사를 돌리는 식으로 바꿔보았습니다.
created_at: 2025-02-26T10:14:41Z
author_association: NONE
diff_hunk: @@ -1,18 +1,29 @@
 package lotto.model
 
-class PrizeCalculator {
-    fun calculateInputMoney(lottoList: List<Lotto>): Int {
-        val allAmount = 0
-        lottoList.forEach {
-            allAmount + it.prize.money
+class PrizeCalculator(
+    val winningLotto: WinningLotto,
+    val publishedLotto: List<Lotto>,
+    val amount: Amount,
+) {
+    val result: Map<Rank, Int>
path: src/main/kotlin/lotto/model/PrizeCalculator.kt
user: login: Leeyerin0210
body: 사실 완전히 이해는 못했습니다만..

클래스를 쓰는 곳에서 그 객체를 활용하여 코드들을 호출하려고 하는게 메세지를 보낸다라는 거라면, 그 메세지를 받아올 수 있도록 메소드로 제공해야 하는 것이 맞을까요? 그렇기에 가급적 메서드는 기능을 직관적으로 볼 수 있게 이름을 작성해야 하겠네요!
created_at: 2025-02-26T10:23:38Z
author_association: NONE
diff_hunk: @@ -1,18 +1,29 @@
 package lotto.model
 
-class PrizeCalculator {
-    fun calculateInputMoney(lottoList: List<Lotto>): Int {
-        val allAmount = 0
-        lottoList.forEach {
-            allAmount + it.prize.money
+class PrizeCalculator(
+    val winningLotto: WinningLotto,
+    val publishedLotto: List<Lotto>,
+    val amount: Amount,
+) {
+    val result: Map<Rank, Int>
path: src/main/kotlin/lotto/model/PrizeCalculator.kt
user: login: laco-dev
body: 아래 함수는 어떤 기능을 할 것으로 보이시나요?
```kotlin
fun getWinningCountBy(rank: Rank): Int 
```
created_at: 2025-02-26T13:43:13Z
author_association: NONE
diff_hunk: @@ -3,51 +3,111 @@ package lotto.controller
 import lotto.model.Amount
 import lotto.model.Lotto
 import lotto.model.LottoMachine
-import lotto.model.LottoMatcher
 import lotto.model.LottoNumber
 import lotto.model.LottoNumbers
 import lotto.model.PrizeCalculator
+import lotto.model.Rank
+import lotto.model.ValidationResult
+import lotto.model.WinningLotto
 import lotto.view.InputView
 import lotto.view.OutputView
 
 class LottoController(
     private val inputView: InputView = InputView(),
     private val outputView: OutputView = OutputView(),
 ) {
+    val lottoMachine = LottoMachine()
+    lateinit var money: Amount
path: src/main/kotlin/lotto/controller/LottoController.kt
user: login: laco-dev
body: 너무 상태에 매몰되신 것 같아요.
조금 진정하고 이렇게 생각해보는건 어떨까요?

> 수동 로또 3장을 구매한다.
수동 로또 번호를 3번 입력받는다.
남은 금액만큼 자동 로또를 구매한다.

로또를 발급하는 과정에서는 돈을 매번 줄여가면서 계산할 이유가 없으니까요.
14,000원 -> 수동 로또 3장 -> 자동 로또 11장
created_at: 2025-02-26T13:57:24Z
author_association: NONE
diff_hunk: @@ -3,51 +3,111 @@ package lotto.controller
 import lotto.model.Amount
 import lotto.model.Lotto
 import lotto.model.LottoMachine
-import lotto.model.LottoMatcher
 import lotto.model.LottoNumber
 import lotto.model.LottoNumbers
 import lotto.model.PrizeCalculator
+import lotto.model.Rank
+import lotto.model.ValidationResult
+import lotto.model.WinningLotto
 import lotto.view.InputView
 import lotto.view.OutputView
 
 class LottoController(
     private val inputView: InputView = InputView(),
     private val outputView: OutputView = OutputView(),
 ) {
+    val lottoMachine = LottoMachine()
+    lateinit var money: Amount
+
     fun run() {
-        val amount = getAmount()
-        val lottoMachine = LottoMachine(amount)
-        val publishedLotto = publishLotto(lottoMachine)
-        val winningLotto = getWinningLotto()
-        val bonusNumber = getBonusNumber()
-        val lottoMatcher = LottoMatcher(winningLotto, bonusNumber)
-        showEarningRate(amount, lottoMatcher, publishedLotto)
+        money = getAmount()
+        val publishedLotto = publishLottoList(inputView.getManualCount()) + autoPublishLotto()
+        val winningLotto =
+            publishWinningLotto(
+                validatePublishLotto(inputView.getWinningLotto()),
+                validateBonusNumber(inputView.getBonusNumber()),
+            )
path: src/main/kotlin/lotto/controller/LottoController.kt
user: login: laco-dev
body: 아직 로또 게임에 대한 시나리오가 뭘 하고 싶은 것인지 정리정돈이 안된 느낌이 듭니다.
publish()함수는 발행 뿐만 아니라 유효성 검사나 출력 외에도 이것저것 기능을 많이 하고 있습니다.
그런데 함수에서 또 유효성 검사도 섞여있어요.

점점 어려워 하시는 것 같아서 힌트를 드려가면서 의견을 드리겠습니다.
슈도 코드 형식으로 인터페이스만 작성하였고 이를 참고해서 run() 함수를 정리해보면 좋을 것 같아요
```kotlin
val lottoPrice = Amount(1000)

val amount: Amount = inputAmount()
val manualLottoCount: Int = inputManualLottoCount()
val autoLottoCount: Int = amount.getCount(lottoPrice) - manualLottoCount

val manualLottoList: List<Lotto> = publishManualLottoList(manualLottoCount)
val autoLottoList: List<Lotto> = publishAutoLottoList(autoLottoCount)
val winningLotto: WinningLotto = publishWinningLotto()

val myLottoList = manualLottoList + autoLottoList
printMyLottoList(count, count, myLottoList)

val statistics: LottoStatistics = winningLotto.match(myLottoList)
printLottoStatistics(statistics)
```
created_at: 2025-02-26T14:07:06Z
author_association: NONE
diff_hunk: @@ -3,51 +3,111 @@ package lotto.controller
 import lotto.model.Amount
 import lotto.model.Lotto
 import lotto.model.LottoMachine
-import lotto.model.LottoMatcher
 import lotto.model.LottoNumber
 import lotto.model.LottoNumbers
 import lotto.model.PrizeCalculator
+import lotto.model.Rank
+import lotto.model.ValidationResult
+import lotto.model.WinningLotto
 import lotto.view.InputView
 import lotto.view.OutputView
 
 class LottoController(
     private val inputView: InputView = InputView(),
     private val outputView: OutputView = OutputView(),
 ) {
+    val lottoMachine = LottoMachine()
+    lateinit var money: Amount
+
     fun run() {
-        val amount = getAmount()
-        val lottoMachine = LottoMachine(amount)
-        val publishedLotto = publishLotto(lottoMachine)
-        val winningLotto = getWinningLotto()
-        val bonusNumber = getBonusNumber()
-        val lottoMatcher = LottoMatcher(winningLotto, bonusNumber)
-        showEarningRate(amount, lottoMatcher, publishedLotto)
+        money = getAmount()
+        val publishedLotto = publishLottoList(inputView.getManualCount()) + autoPublishLotto()
+        val winningLotto =
+            publishWinningLotto(
+                validatePublishLotto(inputView.getWinningLotto()),
+                validateBonusNumber(inputView.getBonusNumber()),
+            )
+        val prizeCalculator = PrizeCalculator(winningLotto, publishedLotto)
+        showEarningRate(prizeCalculator)
     }
 
     private fun getAmount(): Amount = Amount(inputView.getMoney())
 
-    private fun publishLotto(lottoMachine: LottoMachine): List<Lotto> {
-        val publishedLotto = lottoMachine.publishLottoTickets(Amount(LOTTO_PRIZE))
+    private fun publishWinningLotto(
+        numbers: LottoNumbers,
+        bonus: LottoNumber,
+    ): WinningLotto {
+        if (WinningLotto.validation(numbers, bonus) == ValidationResult.Error.DuplicateError) {
+            return publishWinningLotto(
+                numbers,
+                validateBonusNumber(inputView.getBonusNumberRepeat()),
+            )
+        }
+
+        return WinningLotto(numbers, bonus)
+    }
+
+    private fun validatePublishLotto(input: List<Int>): LottoNumbers {
+        val validationResult = LottoNumbers.validation(input)
+        return when (validationResult) {
+            ValidationResult.Error.NumberRangeError ->
+                validatePublishLotto(
+                    inputView.getManualLottoRepeat(),
+                )
+
+            ValidationResult.Error.DuplicateError ->
+                validatePublishLotto(
+                    inputView.getManualLottoRepeat(),
+                )
+
+            ValidationResult.Error.NumberSizeError ->
+                validatePublishLotto(
+                    inputView.getManualLottoRepeat(),
+                )
+
+            else -> {
+                LottoNumbers(input.map { it -> LottoNumber(it) })
+            }
+        }
+    }
+
+    private fun publishLottoList(count: Int): List<Lotto> {
+        val useMoney = Amount(count * LOTTO_PRIZE)
+        if (money.validate(useMoney) == ValidationResult.Error.OverMoneyError) return publishLottoList(inputView.getManualCountRepeat())
+        money = money.payMoney(useMoney)
+        inputView.lottoInputMessage()
+        var lottoList: MutableList<Lotto> = mutableListOf()
+        repeat(count) {
+            lottoList.add(Lotto(validatePublishLotto(inputView.getManualLotto())))
+        }
path: src/main/kotlin/lotto/controller/LottoController.kt
user: login: laco-dev
body: `List(size) { ... }` Builder 함수를 찾아보세요.
created_at: 2025-02-26T14:09:03Z
author_association: NONE
diff_hunk: @@ -5,14 +5,19 @@ value class LottoNumber(
     val value: Int,
 ) {
     init {
-        require(value in MIN_VALUE..MAX_VALUE) {
-            RANGE_ERROR_MESSAGE
-        }
+        require(value in MIN_VALUE..MAX_VALUE) { NUMBER_RANGE_ERROR }
     }
 
     companion object {
         const val MIN_VALUE = 1
         const val MAX_VALUE = 45
-        private const val RANGE_ERROR_MESSAGE = "[ERROR] 로또 번호는 1~45 사이여야 합니다."
+        const val NUMBER_RANGE_ERROR = "[ERROR] 로또 숫자가 범위를 벗어났습니다."
+
+        fun validation(number: Int): ValidationResult {
+            if (number !in MIN_VALUE..MAX_VALUE) {
+                return ValidationResult.Error.NumberRangeError
+            }
+            return ValidationResult.Success
+        }
path: src/main/kotlin/lotto/model/LottoNumber.kt
user: login: laco-dev
body: 외부에서 validation을 호출해야 한다는 사실 자체를 모를거에요. 그렇죠?
LottoNumber가 null 타입을 허용할지 아니면 그냥 오류로 처리할지에 따라 핸들링 하는 부분은 달라지겠지만,

정적 팩토리 메소드를 사용하는 경우를 예로 들어볼 수 있겠네요.
```kotlin
value class LottoNumber private constructor(val value: Int) {
   
   companion object {
      fun valueOf(value: Int): LottoNumber { ... }
      fun createOrNull(value: Int): LottoNumber? { ... }
   }
}
```
created_at: 2025-02-26T14:12:12Z
author_association: NONE
diff_hunk: @@ -1,22 +1,31 @@
 package lotto.model
 
+import kotlin.collections.distinct
+
 class LottoNumbers(
     val numberList: List<LottoNumber>,
 ) {
     init {
-        require(numberList.size == LOTTO_NUMBER_SIZE) {
-            SIZE_ERROR_MESSAGE
-        }
-        require(numberList.distinct().size == numberList.size) {
-            DUPLICATE_ERROR_MESSAGE
-        }
+        require(numberList.size == LOTTO_NUMBER_SIZE) { NUMBER_SIZE_ERROR }
+        require(numberList.distinct().size == numberList.size) { NUMBER_DUPLICATE_ERROR }
     }
 
     fun include(number: LottoNumber): Boolean = numberList.contains(number)
 
     companion object {
         const val LOTTO_NUMBER_SIZE = 6
-        private const val SIZE_ERROR_MESSAGE = "[ERROR] 로또 번호는 6개여야 합니다."
-        private const val DUPLICATE_ERROR_MESSAGE = "[ERROR] 로또 번호는 중복될 수 없습니다."
+        const val NUMBER_SIZE_ERROR = "[ERROR] 로또 번호의 갯수가 ${LOTTO_NUMBER_SIZE}개가 아닙니다"
+        const val NUMBER_DUPLICATE_ERROR = "[ERROR] 로또 번호가 중복되었습니다."
+
+        fun validation(numberList: List<Int>): ValidationResult {
+            when {
+                numberList.size != LOTTO_NUMBER_SIZE -> return ValidationResult.Error.NumberSizeError
+                numberList.distinct().size != numberList.size -> return ValidationResult.Error.DuplicateError
+            }
+            numberList.forEach {
+                if (LottoNumber.validation(it) == ValidationResult.Error.NumberRangeError) return ValidationResult.Error.NumberRangeError
+            }
+            return ValidationResult.Success
+        }
path: src/main/kotlin/lotto/model/LottoNumbers.kt
user: login: laco-dev
body: 마찬가지로 외부에서 validation을 호출하지 않는다면 의미없는 함수가 되버립니다.
sealed class를 이용한 예외 처리는 더욱 어려운 개념이기에 이를 이용한 예시를 보여드리면 아래와 같습니다.

지금은 이렇게까지 떠올리지 못하더라도 정상이니 편히 생각해주세요.
그러기 위해서 배우고 있는 거니까요

```kotlin

sealed interface LottoNumbersResult {
    data class Failure(val throwable: Throwable): LottoNumbersResult
    data class Success(val value: LottoNumbers): LottoNumbersResult
}


fun foo(...): LottoNumbersResult {
   runCatching { LottoNumbers(...) }
     fold(
       onSuccess = { LottoNumbersResult.Success(it) },
       onFailure = { ... }
    )
}
```

만들어놓고도 아쉽네요..
일반적으로 try, catch를 안쓴다는 것이지 지금은 쓰는게 훨씬 구조가 깔끔하게 나올 것이다 라는 의견입니다.
애초에 명확한 논리 그 자체인 모델들을 억지로 성공/실패로 나눈다는것도 어색하기는 해요.
저라면 그냥 터트렸습니다. 🤔 
created_at: 2025-02-26T14:21:58Z
author_association: NONE
diff_hunk: @@ -11,32 +11,31 @@ class NormalShuffle : ShuffleStrategy {
 }
 
 class LottoMachine(
-    private val amount: Amount,
     private val shuffle: ShuffleStrategy = NormalShuffle(),
 ) {
     private val numbers: List<Int> = (LottoNumber.MIN_VALUE..LottoNumber.MAX_VALUE).toList()
 
-    fun publishLottoTickets(lottoPrize: Amount): List<Lotto> {
+    fun publishAutoTickets(money: Amount): List<Lotto> {
         val lottoTickets = mutableListOf<Lotto>()
-        val moneyList = amount.moneySplit(lottoPrize)
-        repeat(moneyList.size) {
-            val lotto = publishLotto(lottoPrize)
+        repeat(money.moneySplit(LOTTO_PRIZE).size) {
+            val lotto = publishLotto()
             lottoTickets.add(lotto)
         }
         return lottoTickets
     }
 
-    private fun publishLotto(prize: Amount): Lotto {
+    private fun publishLotto(): Lotto {
         val lottoNumbers =
             shuffle
                 .shuffle(numbers)
                 .take(LOTTO_NUMBER_COUNT)
                 .sorted()
                 .map { LottoNumber(it) }
-        return Lotto(LottoNumbers(lottoNumbers), prize)
+        return Lotto(LottoNumbers(lottoNumbers))
     }
 
     companion object {
         private const val LOTTO_NUMBER_COUNT = 6
+        private val LOTTO_PRIZE = Amount(1000)
path: src/main/kotlin/lotto/model/LottoMachine.kt
user: login: laco-dev
body: PrizeCalculator에도 똑같은 값이 있지 않나요?
저는 로또 기계도, 당첨금 계산기도 모두 알 필요 없다고 생각하는 편입니다.
created_at: 2025-02-26T14:34:32Z
author_association: NONE
diff_hunk: @@ -0,0 +1,22 @@
+package lotto
+
+class LottoNumber private constructor(
+    val value: Int,
+) {
+    companion object {
+        private val VALID_RANGE = 1..45
+        private const val RANGE_ERROR = "[ERROR] 범위 외의 값입니다."
+
+        fun valueOf(value: Int): LottoNumber {
+            require(value in VALID_RANGE) { RANGE_ERROR }
+            return LottoNumber(value)
+        }
+
+        fun createOrNull(value: Int): LottoNumber? {
+            if (validation(value)) return LottoNumber(value)
+            return null
+        }
path: src/main/kotlin/lotto/LottoNumber.kt
user: login: laco-dev
body: API 설계자의 입장으로서 바라보면 2가지가 아쉬운데요.

1. 생성 인터페이스에 일관성이 부족하다
하나는 valueOf 다른 하나는 create로 이어지는 흐름이 부자연스러워요.
예를 들어 toInt(), toIntOrNull() 같은 예시를 들 수 있겠죠.

2. private constructor가 만능은 아닙니다.
data class로 만들면 불가능한 옵션이기도 하고 클래스의 논리적인 흐름을 외부로 넘겼을 뿐이라서요.

```kotlin
init { require(...) }

fun valueOf(value: Int): LottoNumber  {
   LottoNumber(...)
}

fun valueOfOrNull(value: Int): LottoNumber ? {
    runCatching { valueOf(value) }
       .getOrNull()
}
```
created_at: 2025-03-03T01:51:08Z
author_association: NONE
diff_hunk: @@ -21,15 +21,17 @@
 ### 로또 번호
 - [x] 로또 번호는 1~45 사이이다.
 ### 로또
-- [ ] 로또 1매에는 6개의 로또 번호가 존재한다.
-- [ ] 로또 번호는 중복되지 않는다.
-- [ ] 로또 번호는 오름차순으로 정렬된다.
+- [x] 로또 1매에는 6개의 로또 번호가 존재한다.
+- [x] 로또 번호는 중복되지 않는다.
+- [x] 로또 번호는 오름차순으로 정렬된다.
path: README.md
user: login: laco-dev
body: > feat : 로또 객체 생성

TDD로 한다면 위 기능 3가지를 모두 별도의 커밋으로 하는게 좋았을 것 같아요.
지금은 이미 완성된 모든 기능을 이해한 채로 객체부터 만들게 되니까요
created_at: 2025-03-03T01:55:33Z
author_association: NONE
diff_hunk: @@ -0,0 +1,21 @@
+package lotto
+
+class Lotto private constructor(
+    val numberList: List<LottoNumber>,
+) {
+    init {
+        require(numberList.sortedBy { it.value } == numberList) { LOTTO_NOT_SORTED }
+    }
+
+    companion object {
+        const val LOTTO_NUMBER_QUANTITY = 6
+        const val LOTTO_NOT_SORTED = "[ERROR] 로또 번호가 정렬되지 않았습니다."
+
+        fun createOrNull(numberList: List<LottoNumber>): Lotto? =
+            when {
+                numberList.size != LOTTO_NUMBER_QUANTITY -> null
+                numberList.distinctBy { it.value }.size != numberList.size -> null
+                else -> Lotto(numberList)
+            }
path: src/main/kotlin/lotto/Lotto.kt
user: login: laco-dev
body: 마찬가지로 정적 팩토리 메서드가 객체를 생성하는 책임 외에도, 비즈니스 로직을 가지기 때문에 책임이 여러개로 나뉘었다고 생각합니다.

생성자에서 이미 유효성 검사를 하고 있다면 굳이 중복할 필요가 없습니다.
created_at: 2025-03-03T01:56:34Z
author_association: NONE
diff_hunk: @@ -0,0 +1,14 @@
+package lotto
+
+class Amount private constructor(
+    val money: Int,
+) {
+    fun getCount(lottoPrize: Int): Int = money / lottoPrize
path: src/main/kotlin/lotto/Amount.kt
user: login: laco-dev
body: Amount를 유연하게 만들었다면, 더이상 "로또" 라는 관심사를 가질 필요가 있을까요?
이 클래스는 LottoAmount가 되어야 했을까요?
created_at: 2025-03-03T01:58:46Z
author_association: NONE
diff_hunk: @@ -5,9 +5,14 @@ class Amount private constructor(
 ) {
     fun getCount(lottoPrize: Int): Int = money / lottoPrize
 
+    fun paymentOrNull(payMoney: Int): Amount? {
+        if (money < payMoney) return null
+        return Amount(money - payMoney)
+    }
path: src/main/kotlin/lotto/Amount.kt
user: login: laco-dev
body: 금액은 음수일 수 없다는 것을 null로 하는게 자연스러운 흐름일까요?
물론 마이너스가 되면 논리적으로 문제가 될 수 있으니 null을 반환할 수 도 있다고 생각하지만.
아무리 빼더라도 0보다 작아지지 않는다는 개념을 적용하면 이렇게 만들어 볼 수도 있습니다.
```kotlin
data class Amount(val money: Int) {
  require(money >= 0)

 fun pay(other: Amount): Amount {
   val newMoney = (money - other.money).coerceAtLeast(0)
   return copy(money = newMoney)
 }
}
```

결과적으로 이 함수를 사용할 때 마다 null을 컨트롤 해야한다는게 매우 번거롭다고 느껴지니까요
created_at: 2025-03-03T02:01:19Z
author_association: NONE
diff_hunk: @@ -0,0 +1,27 @@
+package lotto
+
+enum class Rank(
+    val countOfMatch: Int,
+    val winningMoney: Int,
+    val matchBonus: Boolean,
+) {
path: src/main/kotlin/lotto/Rank.kt
user: login: laco-dev
body: > feat : rank 객채 및 winningLotto 객체 생성

Rank만 만들면 되는데 왜 당첨 로또가 끼어들었나요?
created_at: 2025-03-03T02:03:53Z
author_association: NONE
diff_hunk: @@ -0,0 +1,27 @@
+package lotto
+
+enum class Rank(
+    val countOfMatch: Int,
+    val winningMoney: Int,
+    val matchBonus: Boolean,
+) {
+    FIRST(6, 2_000_000_000, false),
+    SECOND(5, 30_000_000, true),
+    THIRD(5, 1_500_000, false),
+    FOURTH(4, 50_000, false),
+    FIFTH(3, 5_000, false),
+    MISS(0, 0, false),
+    ;
+
+    companion object {
+        fun valueOf(
+            countOfMatch: Int,
+            matchBonus: Boolean,
+        ): Rank =
+            entries.firstOrNull {
+                (it.countOfMatch == countOfMatch) && ((it.matchBonus && matchBonus) == it.matchBonus)
+            } ?: throw IllegalArgumentException(RANK_FIND_ERROR)
+
+        const val RANK_FIND_ERROR = "[ERROR] 순위를 찾을 수 없습니다"
path: src/main/kotlin/lotto/Rank.kt
user: login: laco-dev
body: 현재 커밋에는 가장 기본적인 이 함수 테스트도 빠져있습니다.

> 당첨 갯수가 3개라면 5등이다.
> 당첨 갯수가 5개고 보너스 번호가 일치하면 2등이다.
> ...
created_at: 2025-03-03T02:05:13Z
author_association: NONE
diff_hunk: @@ -0,0 +1,27 @@
+package lotto
+
+enum class Rank(
+    val countOfMatch: Int,
+    val winningMoney: Int,
+    val matchBonus: Boolean,
+) {
+    FIRST(6, 2_000_000_000, false),
+    SECOND(5, 30_000_000, true),
+    THIRD(5, 1_500_000, false),
+    FOURTH(4, 50_000, false),
+    FIFTH(3, 5_000, false),
+    MISS(0, 0, false),
+    ;
+
+    companion object {
+        fun valueOf(
+            countOfMatch: Int,
+            matchBonus: Boolean,
+        ): Rank =
+            entries.firstOrNull {
+                (it.countOfMatch == countOfMatch) && ((it.matchBonus && matchBonus) == it.matchBonus)
+            } ?: throw IllegalArgumentException(RANK_FIND_ERROR)
path: src/main/kotlin/lotto/Rank.kt
user: login: laco-dev
body: 의도치 않은 값이 들어오면 MISS로 던지는게 목적 아니었나요?
created_at: 2025-03-03T02:05:44Z
author_association: NONE
diff_hunk: @@ -1,5 +1,13 @@
 package lotto
 
+interface SortStrategy {
+    fun sort(numberList: List<LottoNumber>): List<LottoNumber>
+}
+
+class RandomSort : SortStrategy {
+    override fun sort(numberList: List<LottoNumber>): List<LottoNumber> = numberList.shuffled()
+}
path: src/main/kotlin/lotto/Lotto.kt
user: login: laco-dev
body: 당첨 로또 만드는데 이 객체는 왜 섞여들어왔을까요?
created_at: 2025-03-03T02:06:27Z
author_association: NONE
diff_hunk: @@ -0,0 +1,23 @@
+package lotto
+
+class WinningLotto private constructor(
+    private val lottoNumbers: List<LottoNumber>,
+    private val bonusNumber: LottoNumber,
+) {
+    fun findRank(lotto: Lotto): Rank {
+        val countOfMatch =
+            lottoNumbers.intersect(lotto.numberList).size
path: src/main/kotlin/lotto/WinningLotto.kt
user: login: laco-dev
body: 몇개가 일치하는지는 로또가 하는게 더 자연스럽지 않았을까요?

> Test: 로또는 내가 가진 번호중 몇개가 일치하는지 알 수 있다.
created_at: 2025-03-03T02:07:30Z
author_association: NONE
diff_hunk: @@ -0,0 +1,23 @@
+package lotto
+
+class WinningLotto private constructor(
+    private val lottoNumbers: List<LottoNumber>,
+    private val bonusNumber: LottoNumber,
+) {
+    fun findRank(lotto: Lotto): Rank {
+        val countOfMatch =
+            lottoNumbers.intersect(lotto.numberList).size
+        val bonusMatched = lotto.numberList.contains(bonusNumber)
path: src/main/kotlin/lotto/WinningLotto.kt
user: login: laco-dev
body: 보너스 번호도 마찬가지로 Lotto가 할 일로 보이네요!

> Test : 로또는 해당 번호가 포함되어있는지 알 수 있다.
created_at: 2025-03-03T02:07:56Z
author_association: NONE
diff_hunk: @@ -10,12 +18,28 @@ class Lotto private constructor(
     companion object {
         const val LOTTO_NUMBER_QUANTITY = 6
         const val LOTTO_NOT_SORTED = "[ERROR] 로또 번호가 정렬되지 않았습니다."
+        const val NUMBER_COUNT_ERROR = "[ERROR] 로또 번호 갯수가 ${LOTTO_NUMBER_QUANTITY}가 아닙니다"
+        const val NUMBER_DUPLICATE_ERROR = "[ERROR] 로또 번호가 중복됩니다."
+        private val LOTTO_NUMBERS: List<LottoNumber> = (1..45).map { it -> LottoNumber.valueOf(it) }
+
+        fun valueOf(numberList: List<LottoNumber>): Lotto {
+            require(numberList.size == LOTTO_NUMBER_QUANTITY) { NUMBER_COUNT_ERROR }
+            require(numberList.distinctBy { it.value }.size == numberList.size) { NUMBER_DUPLICATE_ERROR }
+            return Lotto(numberList)
+        }
 
         fun createOrNull(numberList: List<LottoNumber>): Lotto? =
             when {
                 numberList.size != LOTTO_NUMBER_QUANTITY -> null
                 numberList.distinctBy { it.value }.size != numberList.size -> null
                 else -> Lotto(numberList)
             }
+
+        fun createRandom(sortStrategy: SortStrategy = RandomSort()): Lotto =
+            Lotto(
+                sortStrategy.sort(LOTTO_NUMBERS).slice(0..5).sortedBy {
+                    it.value
+                },
+            )
path: src/main/kotlin/lotto/Lotto.kt
user: login: laco-dev
body: "정렬"이라는 기능만 분리했기 때문에 결국 생성하기 위한 로직은 드러나게 되었어요.
이런 의도라면 "로또를 생성할 수 있다" 라는 역할을 만들고 싶었던 걸까요?

```kotlin
interface LottoMachine {
   fun create(size: Int): List<Lotto>
}

class 수동로또머신
class 자동로또머신
```
created_at: 2025-03-03T02:09:50Z
author_association: NONE
diff_hunk: @@ -0,0 +1,19 @@
+package lotto.domain.service
+
+import lotto.domain.model.Rank
+
+class RankCalculator {
+    fun earningMoney(winningList: List<Rank>): Int = winningList.sumOf { it.winningMoney }
+
+    fun calculateEarningRate(
+        inputMoney: Int,
+        earningMoney: Int,
+    ): Double {
+        require(inputMoney != 0) { INPUT_MONEY_ZERO }
+        return earningMoney.toDouble() / inputMoney.toDouble()
+    }
+
+    companion object {
+        const val INPUT_MONEY_ZERO = "[Error] 입력 금액이 0입니다"
+    }
+}
path: src/main/kotlin/lotto/domain/service/RankCalculator.kt
user: login: laco-dev
body: service 패키지는 뭔가요? model과 무슨 차이가 있는지 쉽게 설명해주세요.
모델이 스스로 해야할 책임을 전부 뺏어가는건 아닌가요?

한편으로는 객체지향에서는 왜  `-er` `-or` 클래스를 지양하라는지 알고 계신가요?
created_at: 2025-03-03T02:12:09Z
author_association: NONE
diff_hunk: @@ -0,0 +1,10 @@
+package lotto.domain.service
+
+import lotto.domain.model.Lotto
+import lotto.domain.model.LottoNumber
+
+class LottoMaker {
+    fun autoLottoMaker(count: Int): List<Lotto> = List<Lotto>(count) { Lotto.createRandom() }
+
+    private fun manualLottoMaker(numberList: List<LottoNumber>): Lotto? = Lotto.createOrNull(numberList)
+}
path: src/main/kotlin/lotto/domain/service/LottoMaker.kt
user: login: laco-dev
body: 랜덤 로또 생성방법을 Lotto객체에 만들었는데 로또메이커가 왜 필요한건가여?
created_at: 2025-03-03T02:13:21Z
author_association: NONE
diff_hunk: @@ -0,0 +1,107 @@
+package lotto.controller
+
+import lotto.domain.model.Amount
+import lotto.domain.model.Lotto
+import lotto.domain.model.LottoCreationResult
+import lotto.domain.model.LottoNumber
+import lotto.domain.model.Rank
+import lotto.domain.model.WinningLotto
+import lotto.domain.model.WinningLottoCreationResult
+import lotto.domain.service.RankCalculator
+import lotto.domain.service.WinningListMaker
+import lotto.view.InputView
+import lotto.view.Message
+import lotto.view.OutputView
+
+class LottoController(
+    val inputView: InputView,
+    val outputView: OutputView,
+) {
+    val amount = inputAmount()
+
+    fun run() {
+        val count = getManualCount()
+        val manualLottoList = getLottoList(count)
+        val autoLottoList = getAutoLotto(amount.getCount(LOTTO_PRIZE) - count)
+
+        outputView.printPurchaseResult(manualLottoList, autoLottoList)
+
+        val winningLotto = getWinningLotto()
+        val ranks = WinningListMaker().calculateRanks(winningLotto, manualLottoList + autoLottoList)
+
+        val sortedResults = sortResultsByOriginalRankOrder(ranks)
+        val totalWinnings = RankCalculator().earningMoney(ranks)
+        val earningRate = RankCalculator().calculateEarningRate(amount.money, totalWinnings)
+
+        outputView.printResult(sortedResults, earningRate)
+    }
+
+    fun getAutoLotto(count: Int): List<Lotto> = List(count) { Lotto.createRandom() }
path: src/main/kotlin/lotto/controller/LottoController.kt
user: login: laco-dev
body: 컨트롤러의 함수 접근자를 public으로 열 필요가있을까요?
created_at: 2025-03-03T02:14:04Z
author_association: NONE
diff_hunk: @@ -0,0 +1,107 @@
+package lotto.controller
+
+import lotto.domain.model.Amount
+import lotto.domain.model.Lotto
+import lotto.domain.model.LottoCreationResult
+import lotto.domain.model.LottoNumber
+import lotto.domain.model.Rank
+import lotto.domain.model.WinningLotto
+import lotto.domain.model.WinningLottoCreationResult
+import lotto.domain.service.RankCalculator
+import lotto.domain.service.WinningListMaker
+import lotto.view.InputView
+import lotto.view.Message
+import lotto.view.OutputView
+
+class LottoController(
+    val inputView: InputView,
+    val outputView: OutputView,
+) {
+    val amount = inputAmount()
+
+    fun run() {
+        val count = getManualCount()
+        val manualLottoList = getLottoList(count)
+        val autoLottoList = getAutoLotto(amount.getCount(LOTTO_PRIZE) - count)
+
+        outputView.printPurchaseResult(manualLottoList, autoLottoList)
+
+        val winningLotto = getWinningLotto()
+        val ranks = WinningListMaker().calculateRanks(winningLotto, manualLottoList + autoLottoList)
+
+        val sortedResults = sortResultsByOriginalRankOrder(ranks)
+        val totalWinnings = RankCalculator().earningMoney(ranks)
+        val earningRate = RankCalculator().calculateEarningRate(amount.money, totalWinnings)
+
+        outputView.printResult(sortedResults, earningRate)
+    }
+
+    fun getAutoLotto(count: Int): List<Lotto> = List(count) { Lotto.createRandom() }
+
+    fun getManualCount(): Int {
+        val manualCount = inputView.getManualCount()
+        return if (amount.getCount(LOTTO_PRIZE) >= manualCount) {
+            manualCount
+        } else {
+            outputView.printErrorMessage(Message.errorCountExceeded())
+            getManualCount()
+        }
+    }
+
+    fun getLottoList(count: Int): List<Lotto> {
+        inputView.messageManualLotto()
+        return List(count) { inputLotto() }
+    }
+
+    private fun inputLotto(): Lotto {
+        when (
+            val result =
+                Lotto.create(
+                    inputView
+                        .getManualLotto()
+                        .mapNotNull { LottoNumber.createOrNull(it) }
+                        .sortedBy { it.value },
+                )
+        ) {
+            is LottoCreationResult.Success -> return result.lotto
+            is LottoCreationResult.Failure.InvalidCount -> outputView.printErrorMessage(Message.errorInvalidLotto())
+            is LottoCreationResult.Failure.DuplicatedNumbers -> outputView.printErrorMessage(Message.errorInvalidLotto())
+            is LottoCreationResult.Failure.NotSorted -> outputView.printErrorMessage(Message.errorInvalidLotto())
path: src/main/kotlin/lotto/controller/LottoController.kt
user: login: laco-dev
body: 커스텀 예외를 만들었지만 결국 동일한 출력이 발생하는 거라면 오버엔지니어링으로 보이네요!
이럴거면 굳이 타입을 만들 필요가 있었을까요?

```kotlin
runCatching { 
   val lotto = Lotto.valueOf(...)
}.getOrElse { ... }
```
created_at: 2025-03-03T02:15:32Z
author_association: NONE
diff_hunk: @@ -0,0 +1,107 @@
+package lotto.controller
+
+import lotto.domain.model.Amount
+import lotto.domain.model.Lotto
+import lotto.domain.model.LottoCreationResult
+import lotto.domain.model.LottoNumber
+import lotto.domain.model.Rank
+import lotto.domain.model.WinningLotto
+import lotto.domain.model.WinningLottoCreationResult
+import lotto.domain.service.RankCalculator
+import lotto.domain.service.WinningListMaker
+import lotto.view.InputView
+import lotto.view.Message
+import lotto.view.OutputView
+
+class LottoController(
+    val inputView: InputView,
+    val outputView: OutputView,
+) {
+    val amount = inputAmount()
+
+    fun run() {
+        val count = getManualCount()
+        val manualLottoList = getLottoList(count)
+        val autoLottoList = getAutoLotto(amount.getCount(LOTTO_PRIZE) - count)
+
+        outputView.printPurchaseResult(manualLottoList, autoLottoList)
+
+        val winningLotto = getWinningLotto()
+        val ranks = WinningListMaker().calculateRanks(winningLotto, manualLottoList + autoLottoList)
+
+        val sortedResults = sortResultsByOriginalRankOrder(ranks)
+        val totalWinnings = RankCalculator().earningMoney(ranks)
+        val earningRate = RankCalculator().calculateEarningRate(amount.money, totalWinnings)
+
+        outputView.printResult(sortedResults, earningRate)
+    }
+
+    fun getAutoLotto(count: Int): List<Lotto> = List(count) { Lotto.createRandom() }
+
+    fun getManualCount(): Int {
+        val manualCount = inputView.getManualCount()
+        return if (amount.getCount(LOTTO_PRIZE) >= manualCount) {
+            manualCount
+        } else {
+            outputView.printErrorMessage(Message.errorCountExceeded())
+            getManualCount()
+        }
+    }
+
+    fun getLottoList(count: Int): List<Lotto> {
+        inputView.messageManualLotto()
+        return List(count) { inputLotto() }
+    }
+
+    private fun inputLotto(): Lotto {
+        when (
+            val result =
+                Lotto.create(
+                    inputView
+                        .getManualLotto()
+                        .mapNotNull { LottoNumber.createOrNull(it) }
+                        .sortedBy { it.value },
+                )
+        ) {
+            is LottoCreationResult.Success -> return result.lotto
+            is LottoCreationResult.Failure.InvalidCount -> outputView.printErrorMessage(Message.errorInvalidLotto())
+            is LottoCreationResult.Failure.DuplicatedNumbers -> outputView.printErrorMessage(Message.errorInvalidLotto())
+            is LottoCreationResult.Failure.NotSorted -> outputView.printErrorMessage(Message.errorInvalidLotto())
+        }
+        return inputLotto()
+    }
+
+    private fun inputAmount(): Amount {
+        val amount = Amount.createOrNull(inputView.getMoney())
+        return amount ?: run {
+            outputView.printErrorMessage(Message.errorInvalidAmount())
+            inputAmount()
+        }
+    }
+
+    private fun getWinningLotto(): WinningLotto {
+        when (
+            val result =
+                WinningLotto.create(
+                    inputView.getWinningLotto().mapNotNull { LottoNumber.createOrNull(it) },
+                    LottoNumber.createOrNull(inputView.getBonusNumber()) ?: return getWinningLotto(),
+                )
path: src/main/kotlin/lotto/controller/LottoController.kt
user: login: laco-dev
body: 코드 깊이가 너무 들어갔어요.
요구사항에 있듯이 코드 indent가 깊어지지 않도록 정리해보면 좋을 것 같아요
created_at: 2025-03-03T02:17:54Z
author_association: NONE
diff_hunk: @@ -0,0 +1,107 @@
+package lotto.controller
+
+import lotto.domain.model.Amount
+import lotto.domain.model.Lotto
+import lotto.domain.model.LottoCreationResult
+import lotto.domain.model.LottoNumber
+import lotto.domain.model.Rank
+import lotto.domain.model.WinningLotto
+import lotto.domain.model.WinningLottoCreationResult
+import lotto.domain.service.RankCalculator
+import lotto.domain.service.WinningListMaker
+import lotto.view.InputView
+import lotto.view.Message
+import lotto.view.OutputView
+
+class LottoController(
+    val inputView: InputView,
+    val outputView: OutputView,
+) {
+    val amount = inputAmount()
+
+    fun run() {
+        val count = getManualCount()
+        val manualLottoList = getLottoList(count)
+        val autoLottoList = getAutoLotto(amount.getCount(LOTTO_PRIZE) - count)
+
+        outputView.printPurchaseResult(manualLottoList, autoLottoList)
+
+        val winningLotto = getWinningLotto()
+        val ranks = WinningListMaker().calculateRanks(winningLotto, manualLottoList + autoLottoList)
+
+        val sortedResults = sortResultsByOriginalRankOrder(ranks)
+        val totalWinnings = RankCalculator().earningMoney(ranks)
+        val earningRate = RankCalculator().calculateEarningRate(amount.money, totalWinnings)
+
+        outputView.printResult(sortedResults, earningRate)
+    }
+
+    fun getAutoLotto(count: Int): List<Lotto> = List(count) { Lotto.createRandom() }
+
+    fun getManualCount(): Int {
+        val manualCount = inputView.getManualCount()
+        return if (amount.getCount(LOTTO_PRIZE) >= manualCount) {
+            manualCount
+        } else {
+            outputView.printErrorMessage(Message.errorCountExceeded())
+            getManualCount()
+        }
+    }
+
+    fun getLottoList(count: Int): List<Lotto> {
+        inputView.messageManualLotto()
+        return List(count) { inputLotto() }
+    }
+
+    private fun inputLotto(): Lotto {
+        when (
+            val result =
+                Lotto.create(
+                    inputView
+                        .getManualLotto()
+                        .mapNotNull { LottoNumber.createOrNull(it) }
+                        .sortedBy { it.value },
+                )
+        ) {
+            is LottoCreationResult.Success -> return result.lotto
+            is LottoCreationResult.Failure.InvalidCount -> outputView.printErrorMessage(Message.errorInvalidLotto())
+            is LottoCreationResult.Failure.DuplicatedNumbers -> outputView.printErrorMessage(Message.errorInvalidLotto())
+            is LottoCreationResult.Failure.NotSorted -> outputView.printErrorMessage(Message.errorInvalidLotto())
+        }
+        return inputLotto()
+    }
+
+    private fun inputAmount(): Amount {
+        val amount = Amount.createOrNull(inputView.getMoney())
+        return amount ?: run {
+            outputView.printErrorMessage(Message.errorInvalidAmount())
+            inputAmount()
+        }
+    }
+
+    private fun getWinningLotto(): WinningLotto {
+        when (
+            val result =
+                WinningLotto.create(
+                    inputView.getWinningLotto().mapNotNull { LottoNumber.createOrNull(it) },
+                    LottoNumber.createOrNull(inputView.getBonusNumber()) ?: return getWinningLotto(),
+                )
+        ) {
+            is WinningLottoCreationResult.Success -> return result.winningLotto
+            is WinningLottoCreationResult.Failure.NumberSizeError -> outputView.printErrorMessage(Message.errorInvalidWinningNumbers())
+            is WinningLottoCreationResult.Failure.BonusNumberDuplicated -> outputView.printErrorMessage(Message.errorInvalidBonusNumber())
+            is WinningLottoCreationResult.Failure.DuplicatedNumbers -> outputView.printErrorMessage(Message.errorInvalidWinningNumbers())
path: src/main/kotlin/lotto/controller/LottoController.kt
user: login: laco-dev
body: > 또한 출력을 위한 메세지 오브젝트를 만들었는데, 이 오브젝트 내의 값을 컨트롤러가 직접 넣어서 뷰에게 보내는게 괜찮을까요?

왜 괜찮다고 생각하는지 궁금합니다.
Result 타입을 그대로 OutputView로 전달하면 안되는건가요?
created_at: 2025-03-03T02:18:22Z
author_association: NONE
diff_hunk: @@ -0,0 +1,107 @@
+package lotto.controller
+
+import lotto.domain.model.Amount
+import lotto.domain.model.Lotto
+import lotto.domain.model.LottoCreationResult
+import lotto.domain.model.LottoNumber
+import lotto.domain.model.Rank
+import lotto.domain.model.WinningLotto
+import lotto.domain.model.WinningLottoCreationResult
+import lotto.domain.service.RankCalculator
+import lotto.domain.service.WinningListMaker
+import lotto.view.InputView
+import lotto.view.Message
+import lotto.view.OutputView
+
+class LottoController(
+    val inputView: InputView,
+    val outputView: OutputView,
+) {
+    val amount = inputAmount()
+
+    fun run() {
+        val count = getManualCount()
+        val manualLottoList = getLottoList(count)
+        val autoLottoList = getAutoLotto(amount.getCount(LOTTO_PRIZE) - count)
+
+        outputView.printPurchaseResult(manualLottoList, autoLottoList)
+
+        val winningLotto = getWinningLotto()
+        val ranks = WinningListMaker().calculateRanks(winningLotto, manualLottoList + autoLottoList)
+
+        val sortedResults = sortResultsByOriginalRankOrder(ranks)
+        val totalWinnings = RankCalculator().earningMoney(ranks)
+        val earningRate = RankCalculator().calculateEarningRate(amount.money, totalWinnings)
+
+        outputView.printResult(sortedResults, earningRate)
+    }
+
+    fun getAutoLotto(count: Int): List<Lotto> = List(count) { Lotto.createRandom() }
+
+    fun getManualCount(): Int {
+        val manualCount = inputView.getManualCount()
+        return if (amount.getCount(LOTTO_PRIZE) >= manualCount) {
+            manualCount
+        } else {
+            outputView.printErrorMessage(Message.errorCountExceeded())
+            getManualCount()
+        }
+    }
+
+    fun getLottoList(count: Int): List<Lotto> {
+        inputView.messageManualLotto()
+        return List(count) { inputLotto() }
+    }
+
+    private fun inputLotto(): Lotto {
+        when (
+            val result =
+                Lotto.create(
+                    inputView
+                        .getManualLotto()
+                        .mapNotNull { LottoNumber.createOrNull(it) }
+                        .sortedBy { it.value },
+                )
+        ) {
+            is LottoCreationResult.Success -> return result.lotto
+            is LottoCreationResult.Failure.InvalidCount -> outputView.printErrorMessage(Message.errorInvalidLotto())
+            is LottoCreationResult.Failure.DuplicatedNumbers -> outputView.printErrorMessage(Message.errorInvalidLotto())
+            is LottoCreationResult.Failure.NotSorted -> outputView.printErrorMessage(Message.errorInvalidLotto())
+        }
+        return inputLotto()
+    }
+
+    private fun inputAmount(): Amount {
+        val amount = Amount.createOrNull(inputView.getMoney())
+        return amount ?: run {
+            outputView.printErrorMessage(Message.errorInvalidAmount())
+            inputAmount()
+        }
+    }
+
+    private fun getWinningLotto(): WinningLotto {
+        when (
+            val result =
+                WinningLotto.create(
+                    inputView.getWinningLotto().mapNotNull { LottoNumber.createOrNull(it) },
+                    LottoNumber.createOrNull(inputView.getBonusNumber()) ?: return getWinningLotto(),
+                )
+        ) {
+            is WinningLottoCreationResult.Success -> return result.winningLotto
+            is WinningLottoCreationResult.Failure.NumberSizeError -> outputView.printErrorMessage(Message.errorInvalidWinningNumbers())
+            is WinningLottoCreationResult.Failure.BonusNumberDuplicated -> outputView.printErrorMessage(Message.errorInvalidBonusNumber())
+            is WinningLottoCreationResult.Failure.DuplicatedNumbers -> outputView.printErrorMessage(Message.errorInvalidWinningNumbers())
+        }
+        return getWinningLotto()
+    }
+
+    private fun sortResultsByOriginalRankOrder(ranks: List<Rank>): List<Pair<Rank, Int>> =
+        Rank.entries
+            .filter { it != Rank.MISS }
+            .map { rank -> rank to ranks.count { it == rank } }
+            .reversed()
path: src/main/kotlin/lotto/controller/LottoController.kt
user: login: laco-dev
body: 당첨 로또가 몇개인지 계산하는 비즈니스 로직이 왜 컨트롤러에 있을까요?
알고리즘적으로 더 좋은 방법은 없을까요? 지금은 리스트를 몇번 순회하고 있는지 찾아보세요
created_at: 2025-03-03T02:20:11Z
author_association: NONE
diff_hunk: @@ -0,0 +1,107 @@
+package lotto.controller
+
+import lotto.domain.model.Amount
+import lotto.domain.model.Lotto
+import lotto.domain.model.LottoCreationResult
+import lotto.domain.model.LottoNumber
+import lotto.domain.model.Rank
+import lotto.domain.model.WinningLotto
+import lotto.domain.model.WinningLottoCreationResult
+import lotto.domain.service.RankCalculator
+import lotto.domain.service.WinningListMaker
+import lotto.view.InputView
+import lotto.view.Message
+import lotto.view.OutputView
+
+class LottoController(
+    val inputView: InputView,
+    val outputView: OutputView,
+) {
path: src/main/kotlin/lotto/controller/LottoController.kt
user: login: laco-dev
body: > feat : 입출력 기능 추가

이게 어떻게 봐야 입출력 기능추가인가요..?
created_at: 2025-03-03T02:20:41Z
author_association: NONE
diff_hunk: @@ -0,0 +1,22 @@
+package lotto
+
+class LottoNumber private constructor(
+    val value: Int,
+) {
+    companion object {
+        private val VALID_RANGE = 1..45
+        private const val RANGE_ERROR = "[ERROR] 범위 외의 값입니다."
+
+        fun valueOf(value: Int): LottoNumber {
+            require(value in VALID_RANGE) { RANGE_ERROR }
+            return LottoNumber(value)
+        }
+
+        fun createOrNull(value: Int): LottoNumber? {
+            if (validation(value)) return LottoNumber(value)
+            return null
+        }
path: src/main/kotlin/lotto/LottoNumber.kt
user: login: Leeyerin0210
body: [19253ff](https://github.com/woowacourse/kotlin-lotto/pull/135/commits/19253ffb15e0174c227bcb0d29e2a7515bbcf84e)
생성 일관성을 제시해주신 코드를 바탕으로 추가하였습니다! 
created_at: 2025-03-06T00:53:35Z
author_association: NONE
diff_hunk: @@ -0,0 +1,107 @@
+package lotto.controller
+
+import lotto.domain.model.Amount
+import lotto.domain.model.Lotto
+import lotto.domain.model.LottoCreationResult
+import lotto.domain.model.LottoNumber
+import lotto.domain.model.Rank
+import lotto.domain.model.WinningLotto
+import lotto.domain.model.WinningLottoCreationResult
+import lotto.domain.service.RankCalculator
+import lotto.domain.service.WinningListMaker
+import lotto.view.InputView
+import lotto.view.Message
+import lotto.view.OutputView
+
+class LottoController(
+    val inputView: InputView,
+    val outputView: OutputView,
+) {
+    val amount = inputAmount()
+
+    fun run() {
+        val count = getManualCount()
+        val manualLottoList = getLottoList(count)
+        val autoLottoList = getAutoLotto(amount.getCount(LOTTO_PRIZE) - count)
+
+        outputView.printPurchaseResult(manualLottoList, autoLottoList)
+
+        val winningLotto = getWinningLotto()
+        val ranks = WinningListMaker().calculateRanks(winningLotto, manualLottoList + autoLottoList)
+
+        val sortedResults = sortResultsByOriginalRankOrder(ranks)
+        val totalWinnings = RankCalculator().earningMoney(ranks)
+        val earningRate = RankCalculator().calculateEarningRate(amount.money, totalWinnings)
+
+        outputView.printResult(sortedResults, earningRate)
+    }
+
+    fun getAutoLotto(count: Int): List<Lotto> = List(count) { Lotto.createRandom() }
+
+    fun getManualCount(): Int {
+        val manualCount = inputView.getManualCount()
+        return if (amount.getCount(LOTTO_PRIZE) >= manualCount) {
+            manualCount
+        } else {
+            outputView.printErrorMessage(Message.errorCountExceeded())
+            getManualCount()
+        }
+    }
+
+    fun getLottoList(count: Int): List<Lotto> {
+        inputView.messageManualLotto()
+        return List(count) { inputLotto() }
+    }
+
+    private fun inputLotto(): Lotto {
+        when (
+            val result =
+                Lotto.create(
+                    inputView
+                        .getManualLotto()
+                        .mapNotNull { LottoNumber.createOrNull(it) }
+                        .sortedBy { it.value },
+                )
+        ) {
+            is LottoCreationResult.Success -> return result.lotto
+            is LottoCreationResult.Failure.InvalidCount -> outputView.printErrorMessage(Message.errorInvalidLotto())
+            is LottoCreationResult.Failure.DuplicatedNumbers -> outputView.printErrorMessage(Message.errorInvalidLotto())
+            is LottoCreationResult.Failure.NotSorted -> outputView.printErrorMessage(Message.errorInvalidLotto())
+        }
+        return inputLotto()
+    }
+
+    private fun inputAmount(): Amount {
+        val amount = Amount.createOrNull(inputView.getMoney())
+        return amount ?: run {
+            outputView.printErrorMessage(Message.errorInvalidAmount())
+            inputAmount()
+        }
+    }
+
+    private fun getWinningLotto(): WinningLotto {
+        when (
+            val result =
+                WinningLotto.create(
+                    inputView.getWinningLotto().mapNotNull { LottoNumber.createOrNull(it) },
+                    LottoNumber.createOrNull(inputView.getBonusNumber()) ?: return getWinningLotto(),
+                )
+        ) {
+            is WinningLottoCreationResult.Success -> return result.winningLotto
+            is WinningLottoCreationResult.Failure.NumberSizeError -> outputView.printErrorMessage(Message.errorInvalidWinningNumbers())
+            is WinningLottoCreationResult.Failure.BonusNumberDuplicated -> outputView.printErrorMessage(Message.errorInvalidBonusNumber())
+            is WinningLottoCreationResult.Failure.DuplicatedNumbers -> outputView.printErrorMessage(Message.errorInvalidWinningNumbers())
+        }
+        return getWinningLotto()
+    }
+
+    private fun sortResultsByOriginalRankOrder(ranks: List<Rank>): List<Pair<Rank, Int>> =
+        Rank.entries
+            .filter { it != Rank.MISS }
+            .map { rank -> rank to ranks.count { it == rank } }
+            .reversed()
path: src/main/kotlin/lotto/controller/LottoController.kt
user: login: Leeyerin0210
body: [33d22ea](https://github.com/woowacourse/kotlin-lotto/pull/135/commits/33d22ea5c33b64fd4319b4aa3e91da2b99ad08d1)
전의 코드에서는 Rank를 순회하면서 ranks 순회하면서 매번 맞는 값을 찾는 식이었는데, 새로 고친 코드에서는 ranks 한번만 순회해서 맞는 키값에 1을 더하는 식으로 바꾸었습니다!
created_at: 2025-03-06T03:37:18Z
author_association: NONE
diff_hunk: @@ -0,0 +1,107 @@
+package lotto.controller
+
+import lotto.domain.model.Amount
+import lotto.domain.model.Lotto
+import lotto.domain.model.LottoCreationResult
+import lotto.domain.model.LottoNumber
+import lotto.domain.model.Rank
+import lotto.domain.model.WinningLotto
+import lotto.domain.model.WinningLottoCreationResult
+import lotto.domain.service.RankCalculator
+import lotto.domain.service.WinningListMaker
+import lotto.view.InputView
+import lotto.view.Message
+import lotto.view.OutputView
+
+class LottoController(
+    val inputView: InputView,
+    val outputView: OutputView,
+) {
path: src/main/kotlin/lotto/controller/LottoController.kt
user: login: Leeyerin0210
body: 입출력 기능을 실행시키기 위한 컨트롤러도 포함되는 줄 알았습니다. 앞으로는 컨트롤러는 따로 커밋하도록 하겠습니다!
created_at: 2025-03-06T03:41:44Z
author_association: NONE
diff_hunk: @@ -0,0 +1,107 @@
+package lotto.controller
+
+import lotto.domain.model.Amount
+import lotto.domain.model.Lotto
+import lotto.domain.model.LottoCreationResult
+import lotto.domain.model.LottoNumber
+import lotto.domain.model.Rank
+import lotto.domain.model.WinningLotto
+import lotto.domain.model.WinningLottoCreationResult
+import lotto.domain.service.RankCalculator
+import lotto.domain.service.WinningListMaker
+import lotto.view.InputView
+import lotto.view.Message
+import lotto.view.OutputView
+
+class LottoController(
+    val inputView: InputView,
+    val outputView: OutputView,
+) {
+    val amount = inputAmount()
+
+    fun run() {
+        val count = getManualCount()
+        val manualLottoList = getLottoList(count)
+        val autoLottoList = getAutoLotto(amount.getCount(LOTTO_PRIZE) - count)
+
+        outputView.printPurchaseResult(manualLottoList, autoLottoList)
+
+        val winningLotto = getWinningLotto()
+        val ranks = WinningListMaker().calculateRanks(winningLotto, manualLottoList + autoLottoList)
+
+        val sortedResults = sortResultsByOriginalRankOrder(ranks)
+        val totalWinnings = RankCalculator().earningMoney(ranks)
+        val earningRate = RankCalculator().calculateEarningRate(amount.money, totalWinnings)
+
+        outputView.printResult(sortedResults, earningRate)
+    }
+
+    fun getAutoLotto(count: Int): List<Lotto> = List(count) { Lotto.createRandom() }
path: src/main/kotlin/lotto/controller/LottoController.kt
user: login: Leeyerin0210
body: [19253ff](https://github.com/woowacourse/kotlin-lotto/pull/135/commits/19253ffb15e0174c227bcb0d29e2a7515bbcf84e)
함수 접근자 확인하고 private으로 수정했습니다! 기본적으로 public으로 작성하는 안 좋은 습관이 있었던 것 같습니다.
created_at: 2025-03-06T03:50:12Z
author_association: NONE
diff_hunk: @@ -0,0 +1,107 @@
+package lotto.controller
+
+import lotto.domain.model.Amount
+import lotto.domain.model.Lotto
+import lotto.domain.model.LottoCreationResult
+import lotto.domain.model.LottoNumber
+import lotto.domain.model.Rank
+import lotto.domain.model.WinningLotto
+import lotto.domain.model.WinningLottoCreationResult
+import lotto.domain.service.RankCalculator
+import lotto.domain.service.WinningListMaker
+import lotto.view.InputView
+import lotto.view.Message
+import lotto.view.OutputView
+
+class LottoController(
+    val inputView: InputView,
+    val outputView: OutputView,
+) {
+    val amount = inputAmount()
+
+    fun run() {
+        val count = getManualCount()
+        val manualLottoList = getLottoList(count)
+        val autoLottoList = getAutoLotto(amount.getCount(LOTTO_PRIZE) - count)
+
+        outputView.printPurchaseResult(manualLottoList, autoLottoList)
+
+        val winningLotto = getWinningLotto()
+        val ranks = WinningListMaker().calculateRanks(winningLotto, manualLottoList + autoLottoList)
+
+        val sortedResults = sortResultsByOriginalRankOrder(ranks)
+        val totalWinnings = RankCalculator().earningMoney(ranks)
+        val earningRate = RankCalculator().calculateEarningRate(amount.money, totalWinnings)
+
+        outputView.printResult(sortedResults, earningRate)
+    }
+
+    fun getAutoLotto(count: Int): List<Lotto> = List(count) { Lotto.createRandom() }
+
+    fun getManualCount(): Int {
+        val manualCount = inputView.getManualCount()
+        return if (amount.getCount(LOTTO_PRIZE) >= manualCount) {
+            manualCount
+        } else {
+            outputView.printErrorMessage(Message.errorCountExceeded())
+            getManualCount()
+        }
+    }
+
+    fun getLottoList(count: Int): List<Lotto> {
+        inputView.messageManualLotto()
+        return List(count) { inputLotto() }
+    }
+
+    private fun inputLotto(): Lotto {
+        when (
+            val result =
+                Lotto.create(
+                    inputView
+                        .getManualLotto()
+                        .mapNotNull { LottoNumber.createOrNull(it) }
+                        .sortedBy { it.value },
+                )
+        ) {
+            is LottoCreationResult.Success -> return result.lotto
+            is LottoCreationResult.Failure.InvalidCount -> outputView.printErrorMessage(Message.errorInvalidLotto())
+            is LottoCreationResult.Failure.DuplicatedNumbers -> outputView.printErrorMessage(Message.errorInvalidLotto())
+            is LottoCreationResult.Failure.NotSorted -> outputView.printErrorMessage(Message.errorInvalidLotto())
+        }
+        return inputLotto()
+    }
+
+    private fun inputAmount(): Amount {
+        val amount = Amount.createOrNull(inputView.getMoney())
+        return amount ?: run {
+            outputView.printErrorMessage(Message.errorInvalidAmount())
+            inputAmount()
+        }
+    }
+
+    private fun getWinningLotto(): WinningLotto {
+        when (
+            val result =
+                WinningLotto.create(
+                    inputView.getWinningLotto().mapNotNull { LottoNumber.createOrNull(it) },
+                    LottoNumber.createOrNull(inputView.getBonusNumber()) ?: return getWinningLotto(),
+                )
+        ) {
+            is WinningLottoCreationResult.Success -> return result.winningLotto
+            is WinningLottoCreationResult.Failure.NumberSizeError -> outputView.printErrorMessage(Message.errorInvalidWinningNumbers())
+            is WinningLottoCreationResult.Failure.BonusNumberDuplicated -> outputView.printErrorMessage(Message.errorInvalidBonusNumber())
+            is WinningLottoCreationResult.Failure.DuplicatedNumbers -> outputView.printErrorMessage(Message.errorInvalidWinningNumbers())
path: src/main/kotlin/lotto/controller/LottoController.kt
user: login: Leeyerin0210
body: result 타입을 그대로 뷰에 전달하면 뷰가 너무 도메인에 대한 세부 정보를 알게 되는 것이 아닌가..해서 이렇게 작성했습니다!
created_at: 2025-03-06T03:58:05Z
author_association: NONE
diff_hunk: @@ -21,15 +21,17 @@
 ### 로또 번호
 - [x] 로또 번호는 1~45 사이이다.
 ### 로또
-- [ ] 로또 1매에는 6개의 로또 번호가 존재한다.
-- [ ] 로또 번호는 중복되지 않는다.
-- [ ] 로또 번호는 오름차순으로 정렬된다.
+- [x] 로또 1매에는 6개의 로또 번호가 존재한다.
+- [x] 로또 번호는 중복되지 않는다.
+- [x] 로또 번호는 오름차순으로 정렬된다.
path: README.md
user: login: Leeyerin0210
body: 유념하겠습니다!
created_at: 2025-03-06T04:04:06Z
author_association: NONE
diff_hunk: @@ -0,0 +1,19 @@
+package lotto.domain.service
+
+import lotto.domain.model.Rank
+
+class RankCalculator {
+    fun earningMoney(winningList: List<Rank>): Int = winningList.sumOf { it.winningMoney }
+
+    fun calculateEarningRate(
+        inputMoney: Int,
+        earningMoney: Int,
+    ): Double {
+        require(inputMoney != 0) { INPUT_MONEY_ZERO }
+        return earningMoney.toDouble() / inputMoney.toDouble()
+    }
+
+    companion object {
+        const val INPUT_MONEY_ZERO = "[Error] 입력 금액이 0입니다"
+    }
+}
path: src/main/kotlin/lotto/domain/service/RankCalculator.kt
user: login: Leeyerin0210
body: 다른 분들이 service 패키지를 작성하는 것을 보고 참고해보았습니다. model이 뭔가 값을 가지고 있는 하나의 객체라는 느낌이라면, service는 비지니스 로직이 주로 이루어진, 동작하는 객체라고 생각하고 작성해보았으나, 객체가 하는 행동을 어느정도 뺐고 있을 수는 있겠네요. 

지양하는 클래스 이름에 대해서 찾아보았습니다. -er/-or 클래스 이름은 "매니저(manager)", "컨트롤러(controller)"처럼 단순히 어떤 일을 하는 객체임을 암시하는 경우가 많다. 이러한 여러 책임을 모아둔 막대한 서비스 객체는 객체지향에 부합하지 않다. 라는 의견을 보았는데, 한 객체당 한 책임만 가지고 있어야 하니 저런 객체는 최대한 작성하지 않는게 좋겠네요.
created_at: 2025-03-09T08:31:50Z
author_association: NONE
diff_hunk: @@ -0,0 +1,107 @@
+package lotto.controller
+
+import lotto.domain.model.Amount
+import lotto.domain.model.Lotto
+import lotto.domain.model.LottoCreationResult
+import lotto.domain.model.LottoNumber
+import lotto.domain.model.Rank
+import lotto.domain.model.WinningLotto
+import lotto.domain.model.WinningLottoCreationResult
+import lotto.domain.service.RankCalculator
+import lotto.domain.service.WinningListMaker
+import lotto.view.InputView
+import lotto.view.Message
+import lotto.view.OutputView
+
+class LottoController(
+    val inputView: InputView,
+    val outputView: OutputView,
+) {
+    val amount = inputAmount()
+
+    fun run() {
+        val count = getManualCount()
+        val manualLottoList = getLottoList(count)
+        val autoLottoList = getAutoLotto(amount.getCount(LOTTO_PRIZE) - count)
+
+        outputView.printPurchaseResult(manualLottoList, autoLottoList)
+
+        val winningLotto = getWinningLotto()
+        val ranks = WinningListMaker().calculateRanks(winningLotto, manualLottoList + autoLottoList)
+
+        val sortedResults = sortResultsByOriginalRankOrder(ranks)
+        val totalWinnings = RankCalculator().earningMoney(ranks)
+        val earningRate = RankCalculator().calculateEarningRate(amount.money, totalWinnings)
+
+        outputView.printResult(sortedResults, earningRate)
+    }
+
+    fun getAutoLotto(count: Int): List<Lotto> = List(count) { Lotto.createRandom() }
+
+    fun getManualCount(): Int {
+        val manualCount = inputView.getManualCount()
+        return if (amount.getCount(LOTTO_PRIZE) >= manualCount) {
+            manualCount
+        } else {
+            outputView.printErrorMessage(Message.errorCountExceeded())
+            getManualCount()
+        }
+    }
+
+    fun getLottoList(count: Int): List<Lotto> {
+        inputView.messageManualLotto()
+        return List(count) { inputLotto() }
+    }
+
+    private fun inputLotto(): Lotto {
+        when (
+            val result =
+                Lotto.create(
+                    inputView
+                        .getManualLotto()
+                        .mapNotNull { LottoNumber.createOrNull(it) }
+                        .sortedBy { it.value },
+                )
+        ) {
+            is LottoCreationResult.Success -> return result.lotto
+            is LottoCreationResult.Failure.InvalidCount -> outputView.printErrorMessage(Message.errorInvalidLotto())
+            is LottoCreationResult.Failure.DuplicatedNumbers -> outputView.printErrorMessage(Message.errorInvalidLotto())
+            is LottoCreationResult.Failure.NotSorted -> outputView.printErrorMessage(Message.errorInvalidLotto())
path: src/main/kotlin/lotto/controller/LottoController.kt
user: login: Leeyerin0210
body: [00d4c70](https://github.com/woowacourse/kotlin-lotto/pull/135/commits/00d4c708c8ba4b792f976063962cae3122b9dc06)

동일한 출력이 발생하지 않게, 출력 메세지를 세분화하였습니다.
created_at: 2025-03-09T08:32:59Z
author_association: NONE
diff_hunk: @@ -0,0 +1,14 @@
+package lotto
+
+class Amount private constructor(
+    val money: Int,
+) {
+    fun getCount(lottoPrize: Int): Int = money / lottoPrize
path: src/main/kotlin/lotto/Amount.kt
user: login: Leeyerin0210
body: [bc33c35](https://github.com/woowacourse/kotlin-lotto/pull/135/commits/bc33c3540a64646ab1b7d242c31e375014e0e726)

더 유연하게 바꾸어보았습니다!
created_at: 2025-03-13T02:17:44Z
author_association: NONE
diff_hunk: @@ -0,0 +1,21 @@
+package lotto
+
+class Lotto private constructor(
+    val numberList: List<LottoNumber>,
+) {
+    init {
+        require(numberList.sortedBy { it.value } == numberList) { LOTTO_NOT_SORTED }
+    }
+
+    companion object {
+        const val LOTTO_NUMBER_QUANTITY = 6
+        const val LOTTO_NOT_SORTED = "[ERROR] 로또 번호가 정렬되지 않았습니다."
+
+        fun createOrNull(numberList: List<LottoNumber>): Lotto? =
+            when {
+                numberList.size != LOTTO_NUMBER_QUANTITY -> null
+                numberList.distinctBy { it.value }.size != numberList.size -> null
+                else -> Lotto(numberList)
+            }
path: src/main/kotlin/lotto/Lotto.kt
user: login: Leeyerin0210
body: [00d4c70](https://github.com/woowacourse/kotlin-lotto/pull/135/commits/00d4c708c8ba4b792f976063962cae3122b9dc06)

생성자에서만 유효성 검사를 하도록 수정하였습니다.
created_at: 2025-03-13T02:21:19Z
author_association: NONE
diff_hunk: @@ -5,9 +5,14 @@ class Amount private constructor(
 ) {
     fun getCount(lottoPrize: Int): Int = money / lottoPrize
 
+    fun paymentOrNull(payMoney: Int): Amount? {
+        if (money < payMoney) return null
+        return Amount(money - payMoney)
+    }
path: src/main/kotlin/lotto/Amount.kt
user: login: Leeyerin0210
body: 금액을 계산할때 계산해야할 금액보다 적은 돈을 낸다면 그 돈을 받고 모자란 돈을 더 달라고 하기 보다 계산을 거부하는 것이 더 맞다고 생각하여 null를 적용시켰습니다. 아무리 빼도 0보다 작아지진 않지만, 0에서 더 빼려고 하면 불가능하다는 것을 표현하고 싶었습니다.
created_at: 2025-03-13T02:25:29Z
author_association: NONE
diff_hunk: @@ -0,0 +1,27 @@
+package lotto
+
+enum class Rank(
+    val countOfMatch: Int,
+    val winningMoney: Int,
+    val matchBonus: Boolean,
+) {
path: src/main/kotlin/lotto/Rank.kt
user: login: Leeyerin0210
body: 로또객체와 보너스 넘버를 가지고 있는 당첨 로또라는 객체가 있으면 더 비교하기 쉽지 않을까 싶어 생성하였습니다만, 다른 피드백을 보니 충분히 없이도 로또 객체에서 비교할 수 있겠네요..!
created_at: 2025-03-13T02:27:44Z
author_association: NONE
diff_hunk: @@ -0,0 +1,27 @@
+package lotto
+
+enum class Rank(
+    val countOfMatch: Int,
+    val winningMoney: Int,
+    val matchBonus: Boolean,
+) {
+    FIRST(6, 2_000_000_000, false),
+    SECOND(5, 30_000_000, true),
+    THIRD(5, 1_500_000, false),
+    FOURTH(4, 50_000, false),
+    FIFTH(3, 5_000, false),
+    MISS(0, 0, false),
+    ;
+
+    companion object {
+        fun valueOf(
+            countOfMatch: Int,
+            matchBonus: Boolean,
+        ): Rank =
+            entries.firstOrNull {
+                (it.countOfMatch == countOfMatch) && ((it.matchBonus && matchBonus) == it.matchBonus)
+            } ?: throw IllegalArgumentException(RANK_FIND_ERROR)
+
+        const val RANK_FIND_ERROR = "[ERROR] 순위를 찾을 수 없습니다"
path: src/main/kotlin/lotto/Rank.kt
user: login: Leeyerin0210
body: [c4552cd](https://github.com/woowacourse/kotlin-lotto/pull/135/commits/c4552cd7facf85f87f0be75f5091bbc52569a224)
해당 테스트 추가했습니다. 리턴하는지만 확인하는 것보다는 값을 정확하게 확인하는 것이 맞는 것 같습니다.
created_at: 2025-03-13T02:38:27Z
author_association: NONE
diff_hunk: @@ -0,0 +1,27 @@
+package lotto
+
+enum class Rank(
+    val countOfMatch: Int,
+    val winningMoney: Int,
+    val matchBonus: Boolean,
+) {
+    FIRST(6, 2_000_000_000, false),
+    SECOND(5, 30_000_000, true),
+    THIRD(5, 1_500_000, false),
+    FOURTH(4, 50_000, false),
+    FIFTH(3, 5_000, false),
+    MISS(0, 0, false),
+    ;
+
+    companion object {
+        fun valueOf(
+            countOfMatch: Int,
+            matchBonus: Boolean,
+        ): Rank =
+            entries.firstOrNull {
+                (it.countOfMatch == countOfMatch) && ((it.matchBonus && matchBonus) == it.matchBonus)
+            } ?: throw IllegalArgumentException(RANK_FIND_ERROR)
path: src/main/kotlin/lotto/Rank.kt
user: login: Leeyerin0210
body: 꽝과 논리 오류는 분리하는것이 맞다고 생각했습니다. 어찌되었든 해당하는 당첨 결과를 찾은 것과 논리적으로 말이 안 되는 값이 나온 것을 같이 취급하면 문제가 될 수 있다고 생각하였습니다.
created_at: 2025-03-13T02:40:02Z
author_association: NONE
diff_hunk: @@ -1,5 +1,13 @@
 package lotto
 
+interface SortStrategy {
+    fun sort(numberList: List<LottoNumber>): List<LottoNumber>
+}
+
+class RandomSort : SortStrategy {
+    override fun sort(numberList: List<LottoNumber>): List<LottoNumber> = numberList.shuffled()
+}
path: src/main/kotlin/lotto/Lotto.kt
user: login: Leeyerin0210
body: 인터페이스의 코드가 매우 짧고, 해당 인터페이스와 클래스는 랜덤 객체를 위한 객체라서 같은 파일에 넣어도 괜찮다고 생각했습니다. 혹시 무조건 객체는 파일을 분리해야 하나요?
created_at: 2025-03-13T02:42:03Z
author_association: NONE
diff_hunk: @@ -0,0 +1,23 @@
+package lotto
+
+class WinningLotto private constructor(
+    private val lottoNumbers: List<LottoNumber>,
+    private val bonusNumber: LottoNumber,
+) {
+    fun findRank(lotto: Lotto): Rank {
+        val countOfMatch =
+            lottoNumbers.intersect(lotto.numberList).size
path: src/main/kotlin/lotto/WinningLotto.kt
user: login: Leeyerin0210
body: [0a0c08b](https://github.com/woowacourse/kotlin-lotto/pull/135/commits/0a0c08b7dd1dacc85f4ca26db1b76c12e9926e32)
해당 사항 적용했습니다!
created_at: 2025-03-13T04:06:36Z
author_association: NONE
diff_hunk: @@ -0,0 +1,23 @@
+package lotto
+
+class WinningLotto private constructor(
+    private val lottoNumbers: List<LottoNumber>,
+    private val bonusNumber: LottoNumber,
+) {
+    fun findRank(lotto: Lotto): Rank {
+        val countOfMatch =
+            lottoNumbers.intersect(lotto.numberList).size
+        val bonusMatched = lotto.numberList.contains(bonusNumber)
path: src/main/kotlin/lotto/WinningLotto.kt
user: login: Leeyerin0210
body: [0a0c08b](https://github.com/woowacourse/kotlin-lotto/pull/135/commits/0a0c08b7dd1dacc85f4ca26db1b76c12e9926e32)
마찬가지로 적용 완료했습니다.
created_at: 2025-03-13T04:06:46Z
author_association: NONE
diff_hunk: @@ -0,0 +1,27 @@
+package lotto
+
+enum class Rank(
+    val countOfMatch: Int,
+    val winningMoney: Int,
+    val matchBonus: Boolean,
+) {
path: src/main/kotlin/lotto/Rank.kt
user: login: Leeyerin0210
body: [0a0c08b](https://github.com/woowacourse/kotlin-lotto/pull/135/commits/0a0c08b7dd1dacc85f4ca26db1b76c12e9926e32)
당첨 로또 객체를 삭제하고 로또 객체를 활용해서 비교할 수 있도록 하였습니다.
created_at: 2025-03-13T04:07:27Z
author_association: NONE
diff_hunk: @@ -10,12 +18,28 @@ class Lotto private constructor(
     companion object {
         const val LOTTO_NUMBER_QUANTITY = 6
         const val LOTTO_NOT_SORTED = "[ERROR] 로또 번호가 정렬되지 않았습니다."
+        const val NUMBER_COUNT_ERROR = "[ERROR] 로또 번호 갯수가 ${LOTTO_NUMBER_QUANTITY}가 아닙니다"
+        const val NUMBER_DUPLICATE_ERROR = "[ERROR] 로또 번호가 중복됩니다."
+        private val LOTTO_NUMBERS: List<LottoNumber> = (1..45).map { it -> LottoNumber.valueOf(it) }
+
+        fun valueOf(numberList: List<LottoNumber>): Lotto {
+            require(numberList.size == LOTTO_NUMBER_QUANTITY) { NUMBER_COUNT_ERROR }
+            require(numberList.distinctBy { it.value }.size == numberList.size) { NUMBER_DUPLICATE_ERROR }
+            return Lotto(numberList)
+        }
 
         fun createOrNull(numberList: List<LottoNumber>): Lotto? =
             when {
                 numberList.size != LOTTO_NUMBER_QUANTITY -> null
                 numberList.distinctBy { it.value }.size != numberList.size -> null
                 else -> Lotto(numberList)
             }
+
+        fun createRandom(sortStrategy: SortStrategy = RandomSort()): Lotto =
+            Lotto(
+                sortStrategy.sort(LOTTO_NUMBERS).slice(0..5).sortedBy {
+                    it.value
+                },
+            )
path: src/main/kotlin/lotto/Lotto.kt
user: login: Leeyerin0210
body: sortStrategy는 어디까지나 정렬을 위한 전략이라고 생각하여, 별도의 생성 로직은 넣지 않았습니다. createRandom도 생성자라고 생각해서 생성 로직을 넣어도 괜찮다고 생각했는데, 생성 로직 자체를 별도로 분리하는 것이 적합한가요?
created_at: 2025-03-13T04:12:06Z
author_association: NONE
diff_hunk: @@ -0,0 +1,107 @@
+package lotto.controller
+
+import lotto.domain.model.Amount
+import lotto.domain.model.Lotto
+import lotto.domain.model.LottoCreationResult
+import lotto.domain.model.LottoNumber
+import lotto.domain.model.Rank
+import lotto.domain.model.WinningLotto
+import lotto.domain.model.WinningLottoCreationResult
+import lotto.domain.service.RankCalculator
+import lotto.domain.service.WinningListMaker
+import lotto.view.InputView
+import lotto.view.Message
+import lotto.view.OutputView
+
+class LottoController(
+    val inputView: InputView,
+    val outputView: OutputView,
+) {
+    val amount = inputAmount()
+
+    fun run() {
+        val count = getManualCount()
+        val manualLottoList = getLottoList(count)
+        val autoLottoList = getAutoLotto(amount.getCount(LOTTO_PRIZE) - count)
+
+        outputView.printPurchaseResult(manualLottoList, autoLottoList)
+
+        val winningLotto = getWinningLotto()
+        val ranks = WinningListMaker().calculateRanks(winningLotto, manualLottoList + autoLottoList)
+
+        val sortedResults = sortResultsByOriginalRankOrder(ranks)
+        val totalWinnings = RankCalculator().earningMoney(ranks)
+        val earningRate = RankCalculator().calculateEarningRate(amount.money, totalWinnings)
+
+        outputView.printResult(sortedResults, earningRate)
+    }
+
+    fun getAutoLotto(count: Int): List<Lotto> = List(count) { Lotto.createRandom() }
+
+    fun getManualCount(): Int {
+        val manualCount = inputView.getManualCount()
+        return if (amount.getCount(LOTTO_PRIZE) >= manualCount) {
+            manualCount
+        } else {
+            outputView.printErrorMessage(Message.errorCountExceeded())
+            getManualCount()
+        }
+    }
+
+    fun getLottoList(count: Int): List<Lotto> {
+        inputView.messageManualLotto()
+        return List(count) { inputLotto() }
+    }
+
+    private fun inputLotto(): Lotto {
+        when (
+            val result =
+                Lotto.create(
+                    inputView
+                        .getManualLotto()
+                        .mapNotNull { LottoNumber.createOrNull(it) }
+                        .sortedBy { it.value },
+                )
+        ) {
+            is LottoCreationResult.Success -> return result.lotto
+            is LottoCreationResult.Failure.InvalidCount -> outputView.printErrorMessage(Message.errorInvalidLotto())
+            is LottoCreationResult.Failure.DuplicatedNumbers -> outputView.printErrorMessage(Message.errorInvalidLotto())
+            is LottoCreationResult.Failure.NotSorted -> outputView.printErrorMessage(Message.errorInvalidLotto())
+        }
+        return inputLotto()
+    }
+
+    private fun inputAmount(): Amount {
+        val amount = Amount.createOrNull(inputView.getMoney())
+        return amount ?: run {
+            outputView.printErrorMessage(Message.errorInvalidAmount())
+            inputAmount()
+        }
+    }
+
+    private fun getWinningLotto(): WinningLotto {
+        when (
+            val result =
+                WinningLotto.create(
+                    inputView.getWinningLotto().mapNotNull { LottoNumber.createOrNull(it) },
+                    LottoNumber.createOrNull(inputView.getBonusNumber()) ?: return getWinningLotto(),
+                )
path: src/main/kotlin/lotto/controller/LottoController.kt
user: login: Leeyerin0210
body: [0a0c08b](https://github.com/woowacourse/kotlin-lotto/pull/135/commits/0a0c08b7dd1dacc85f4ca26db1b76c12e9926e32)
당첨 로또 객체를 삭제하면서 수정해 보았습니다.
created_at: 2025-03-13T04:17:59Z
author_association: NONE
diff_hunk: @@ -1,5 +1,13 @@
 package lotto
 
+interface SortStrategy {
+    fun sort(numberList: List<LottoNumber>): List<LottoNumber>
+}
+
+class RandomSort : SortStrategy {
+    override fun sort(numberList: List<LottoNumber>): List<LottoNumber> = numberList.shuffled()
+}
path: src/main/kotlin/lotto/Lotto.kt
user: login: laco-dev
body: 긴밀하게 연결되어 있으면 동일한 파일에 있겠지만, Lotto가 이 인터페이스와 연결점이 잘 보이지 않네요.
그런데 코드가 짧으면 한파일에 다 넣어도 되나요...?
created_at: 2025-03-13T10:52:23Z
author_association: NONE

PR comments:
user: login: Leeyerin0210
created_at: 2025-02-26T10:27:04Z
body: 잘못된 입력을 받았을 때 처리하는 것/논리적 오류와 비 논리적 오류 분리를 중점으로 수정해보았습니다!
한 객체을 수정하니 영향받는 다른 객체들을 수정하다보니 한 커밋에 너무 많은 변경사항이 들어갔네요..죄송합니다.
항상 날카로운 리뷰 너무 감사드립니다! 제가 놓치고 있는게 있다면 계속해서 가감없이 말씀해주세요!
author_association: NONE
user: login: Leeyerin0210
created_at: 2025-03-02T10:45:32Z
body: 너무 늦게 완성했습니다. 죄송합니다.
전반적으로 조금 더 보기 좋은 코드가 된 것 같습니다. 
어떻게 하면 객체 별 결합을 더 강하게 할 수 있을까요?
또한 출력을 위한 메세지 오브젝트를 만들었는데, 이 오브젝트 내의 값을 컨트롤러가 직접 넣어서 뷰에게 보내는게 괜찮을까요?
author_association: NONE
user: login: Leeyerin0210
created_at: 2025-03-13T04:25:36Z
body: 죄송합니다 정말 늦었습니다... 눈 앞에 미션이 있으니 자꾸 전의 미션 해결이 미뤄지는 경향이 있네요. 혹시 리뷰어 입장에서는 지금 진행중인 미션에 집중하는게 나은지, 아님 이전 미션을 다 해결하고 진행하는게 더 나은지 조언을 주실 수 있으신가요?

수정 사항과 제 의견을 적어보았습니다. 가감없이 리뷰 부탁드립니다. 
author_association: NONE
user: login: laco-dev
created_at: 2025-03-13T10:48:47Z
body: > 죄송합니다 정말 늦었습니다... 눈 앞에 미션이 있으니 자꾸 전의 미션 해결이 미뤄지는 경향이 있네요. 혹시 리뷰어 입장에서는 지금 진행중인 미션에 집중하는게 나은지, 아님 이전 미션을 다 해결하고 진행하는게 더 나은지 조언을 주실 수 있으신가요?

제가 말씀드렸지 않았나여, 다음 미션을 준비하는데 있어 큰 부담으로 다가올거라고... 😢 
리뷰어 입장은 컨텍스트 스위칭이 최소화 되는게 아무래도 좋겠죠.

그런데 우테코 리뷰 특성상 여러개의 미션을 동시에 리뷰하지 않습니다.
이미 2단계 미션 기한은 종료되었기 때문에 추가로 리뷰는 하지 않고 바로 마무리를 하기 때문이에요 😓 
author_association: NONE