Repository: woowacourse/kotlin-lotto
Pull request #150: [로또 2단계] 모찌 미션 제출합니다.
Author: login: wondroid-world
Created: 2025-02-27T13:54:31Z | Merged: 2025-03-08T14:36:27Z

# 셀프 체크리스트

<!--
모든 항목을 점검한 후 리뷰를 요청하면, 보다 의미 있는 피드백을 받을 수 있습니다.
-->

- [X] 프로그램이 정상적으로 작동하는가?
- [X] 모든 테스트가 통과하는가?
- [X] 이전에 받은 피드백을 모두 반영하였는가?
- [X] 코딩 스타일 가이드를 준수하였는가?
    - [X] IDE 코드 자동 정렬을 적용하였는가?
    - [X] 린트 검사를 통과하였는가?
- [X] 프로그래밍 요구 사항을 준수하였는가?
- [X] `README.md`에 기능 목록을 정리하고 명확히 기술하였는가?

# 어떤 부분에 집중하여 리뷰해야 할까요?

안녕하세요! 두루. 
저는 이번에 객체지향적인 코드를 작성하는 데, 많은 시간을 투자했습니다. 그리고 모든 원시값과 문자열을 포장할려고 노력했습니다. 예외 처리는 아직 미흡하여 추가적으로 보충한 뒤, 다시 pr 하도록 하겠습니다! 그리고 두루가 말한대로, LottoTicketGenerator interface를 만들어서 수동 로또 생성기와 자동 로또 생성기를 구현했습니다.



Diff:
diff --git a/src/main/kotlin/lotto/controller/LottoController.kt b/src/main/kotlin/lotto/controller/LottoController.kt
index 65441431c..c89b3e6a5 100644
--- a/src/main/kotlin/lotto/controller/LottoController.kt
+++ b/src/main/kotlin/lotto/controller/LottoController.kt
@@ -1,42 +1,71 @@
 package lotto.controller
 
-import lotto.contants.LottoRuleConstants
-import lotto.model.LottoMachine
+import lotto.model.LottoIssueType
 import lotto.model.LottoResult
+import lotto.model.LottoStoreCashier
 import lotto.model.LottoTicket
+import lotto.model.LottoTicketIssueManager
 import lotto.model.WinningLotto
-import lotto.view.UserInterface
+import lotto.view.ViewFlow
 
 class LottoController(
-    private val userInterface: UserInterface = UserInterface(),
+    private val viewFlow: ViewFlow = ViewFlow(),
 ) {
     fun run() {
-        val lottoTickets = generateLottoTickets()
+        val possibleToLottoTicketCount = meetLottoStoreCashier()
+        val lottoTickets = getLottoTickets(possibleToLottoTicketCount)
         val winningLotto = getWinningLotto()
-        val lottoResult = winningLotto.getResult(lottoTickets)
-        getResult(lottoResult)
+        getResult(lottoTickets, winningLotto)
     }
 
-    private fun generateLottoTickets(): List<LottoTicket> {
-        val amount = userInterface.inputPurchaseAmount()
-        val count = calculatePurchaseCount(amount)
-        val lottoTickets = LottoMachine().purchase(count)
-        userInterface.printLottoTickets(lottoTickets)
-        return lottoTickets
+    private fun meetLottoStoreCashier(): Int {
+        val money = viewFlow.inputPurchaseAmount()
+        val lottoStoreCashier = LottoStoreCashier(money)
+        val possibleToLottoTicketCount = lottoStoreCashier.calculatePossibleToBuyLottoTicketCount()
+        val customerAnswer = viewFlow.printLottoCount(possibleToLottoTicketCount)
+        if (customerAnswer) {
+            val change = lottoStoreCashier.calculateChange(possibleToLottoTicketCount)
+            viewFlow.printChange(change)
+        }
+        return possibleToLottoTicketCount
     }
 
-    private fun calculatePurchaseCount(amount: Int) = amount / LottoRuleConstants.LOTTO_AMOUNT.value
+    private fun getLottoTickets(possibleToLottoTicketCount: Int): List<LottoTicket> {
+        val customerWantToBuyManualLottoTicketCount = viewFlow.getManualLottoCount()
+        val manualLottoNumbers =
+            if (customerWantToBuyManualLottoTicketCount != 0) {
+                viewFlow.getManualLottoNumbers(customerWantToBuyManualLottoTicketCount)
+            } else {
+                emptyList()
+            }
+        val lottoTicketIssueManager =
+            LottoTicketIssueManager(
+                possibleToLottoTicketCount,
+                customerWantToBuyManualLottoTicketCount,
+                manualLottoNumbers,
+            )
+        val lottoTickets = lottoTicketIssueManager.getLottoTickets(manualLottoNumbers)
+        val autoLottoTicketCount = lottoTicketIssueManager.getAutoLottoTicketCount()
+        viewFlow.printLottoTickets(customerWantToBuyManualLottoTicketCount, autoLottoTicketCount, lottoTickets)
+        return lottoTickets
+    }
 
     private fun getWinningLotto(): WinningLotto {
-        val winningNumbers = userInterface.getWinningNumbers()
-        val bonusNumber = userInterface.getBonusNumber()
-        return WinningLotto(winningNumbers, bonusNumber)
+        val winningNumbers = viewFlow.getWinningNumbers()
+        val bonusNumber = viewFlow.getBonusNumber()
+        val winningLottoTicket = LottoTicket(LottoIssueType.WINNING, winningNumbers)
+        return WinningLotto(winningLottoTicket, bonusNumber)
     }
 
-    private fun getResult(lottoResult: LottoResult) {
+    private fun getResult(
+        lottoTickets: List<LottoTicket>,
+        winningLotto: WinningLotto,
+    ) {
+        val ranks = winningLotto.getRanks(lottoTickets)
+        val lottoResult = LottoResult(ranks)
         val winningStatus = lottoResult.getWinningStatus()
         val profit = lottoResult.calculateProfit()
-        userInterface.printResult(winningStatus)
-        userInterface.printProfit(profit)
+        viewFlow.printResult(winningStatus)
+        viewFlow.printProfit(profit)
     }
 }
diff --git a/src/main/kotlin/lotto/model/AutoLottoTicketGenerator.kt b/src/main/kotlin/lotto/model/AutoLottoTicketGenerator.kt
new file mode 100644
index 000000000..1c5c7ab9b
--- /dev/null
+++ b/src/main/kotlin/lotto/model/AutoLottoTicketGenerator.kt
@@ -0,0 +1,17 @@
+package lotto.model
+
+import lotto.contants.LottoRuleConstants
+
+class AutoLottoTicketGenerator : LottoTicketGenerator {
+    override val type: LottoIssueType = LottoIssueType.AUTO
+
+    override fun generateLottoTicket(): LottoTicket {
+        val lottoNumbers =
+            (LottoRuleConstants.MINIMUM_NUMBER.value..LottoRuleConstants.MAXIMUM_NUMBER.value)
+                .shuffled()
+                .take(LottoRuleConstants.LOTTO_PICK_COUNT.value)
+                .sorted()
+                .map(::LottoNumber)
+        return LottoTicket(LottoIssueType.AUTO, lottoNumbers)
+    }
+}
diff --git a/src/main/kotlin/lotto/model/LottoIssueType.kt b/src/main/kotlin/lotto/model/LottoIssueType.kt
new file mode 100644
index 000000000..812dd9170
--- /dev/null
+++ b/src/main/kotlin/lotto/model/LottoIssueType.kt
@@ -0,0 +1,9 @@
+package lotto.model
+
+enum class LottoIssueType(
+    val issueType: String,
+) {
+    AUTO("자동"),
+    MANUAL("수동"),
+    WINNING("당첨"),
+}
diff --git a/src/main/kotlin/lotto/model/LottoMachine.kt b/src/main/kotlin/lotto/model/LottoMachine.kt
index e6fcf7d37..6cf552a7c 100644
--- a/src/main/kotlin/lotto/model/LottoMachine.kt
+++ b/src/main/kotlin/lotto/model/LottoMachine.kt
@@ -1,5 +1,21 @@
 package lotto.model
 
 class LottoMachine {
-    fun purchase(count: Int): List<LottoTicket> = List(count) { LottoTicket() }
+    fun issueLottoTickets(
+        autoLottoTicketCount: LottoTicketCount,
+        manualLottoNumbers: List<List<Int>>,
+    ): List<LottoTicket> =
+        purchaseManualLottoTickets(manualLottoNumbers) + purchaseAutoLottoTickets(autoLottoTicketCount)
+
+    private fun purchaseAutoLottoTickets(customerWantToBuyAutoLottoTicketCount: LottoTicketCount): List<LottoTicket> {
+        val autoLottoGenerator = AutoLottoTicketGenerator()
+        val autoLottoTickets =
+            List(customerWantToBuyAutoLottoTicketCount.toInt()) { autoLottoGenerator.generateLottoTicket() }
+        return autoLottoTickets
+    }
+
+    private fun purchaseManualLottoTickets(manualLottoNumbers: List<List<Int>>): List<LottoTicket> {
+        val manualLottoTickets = manualLottoNumbers.map { ManualLottoTicketGenerator(it).generateLottoTicket() }
+        return manualLottoTickets
+    }
 }
diff --git a/src/main/kotlin/lotto/model/LottoStoreCashier.kt b/src/main/kotlin/lotto/model/LottoStoreCashier.kt
new file mode 100644
index 000000000..53a734c23
--- /dev/null
+++ b/src/main/kotlin/lotto/model/LottoStoreCashier.kt
@@ -0,0 +1,22 @@
+package lotto.model
+
+import lotto.contants.LottoRuleConstants
+
+class LottoStoreCashier(
+    private val customerMoney: Int,
+) {
+    private val possibleToLottoTicketCount = calculatePossibleToBuyLottoTicketCount()
+
+    init {
+        require(customerMoney >= LottoRuleConstants.LOTTO_AMOUNT.value) { INSUFFICIENT_MONEY_FOR_LOTTO_PURCHASE }
+    }
+
+    fun calculatePossibleToBuyLottoTicketCount(): Int = customerMoney / LottoRuleConstants.LOTTO_AMOUNT.value
+
+    fun calculateChange(customerWantBuyLottoTicketCount: Int): Int =
+        customerMoney - (LottoRuleConstants.LOTTO_AMOUNT.value * possibleToLottoTicketCount)
+
+    companion object {
+        private const val INSUFFICIENT_MONEY_FOR_LOTTO_PURCHASE = "로또는 천원이상 넣어야지 구매 가능합니다."
+    }
+}
diff --git a/src/main/kotlin/lotto/model/LottoTicket.kt b/src/main/kotlin/lotto/model/LottoTicket.kt
index 03647ed55..3685653b2 100644
--- a/src/main/kotlin/lotto/model/LottoTicket.kt
+++ b/src/main/kotlin/lotto/model/LottoTicket.kt
@@ -2,29 +2,36 @@ package lotto.model
 
 import lotto.contants.LottoRuleConstants
 
-class LottoTicket(
-    private val numbers: Set<LottoNumber> = generateLotto(),
+data class LottoTicket(
+    val lottoIssueType: LottoIssueType,
+    private val numbers: List<LottoNumber>,
 ) {
+    private val lottoNumbers: Set<LottoNumber> = numbers.toSet()
+
     init {
-        require(numbers.size == LottoRuleConstants.LOTTO_PICK_COUNT.value)
-        { ERROR_NUMBERS_COUNT }
+        require(numbers.size == LottoRuleConstants.LOTTO_PICK_COUNT.value) { ERROR_NUMBERS_COUNT }
+    }
+
+    fun getSize() = lottoNumbers.size
+
+    fun containsNumber(number: LottoNumber): Boolean {
+        return lottoNumbers.contains(number)
     }
 
-    fun getSize() = numbers.size
+    fun getNumbers(): Set<LottoNumber> {
+        return lottoNumbers
+    }
 
-    fun getNumbers() = numbers
+    fun matchNumbersSize(lottoTicket: LottoTicket): Int = numbers.intersect(lottoTicket.getNumbers()).size
 
     companion object {
         private const val ERROR_NUMBERS_COUNT = "로또 번호의 개수는 6개입니다."
 
-        private fun generateLotto(): Set<LottoNumber> =
-            (LottoRuleConstants.MINIMUM_NUMBER.value..LottoRuleConstants.MAXIMUM_NUMBER.value)
-                .shuffled()
-                .take(LottoRuleConstants.LOTTO_PICK_COUNT.value)
-                .sorted()
-                .map { LottoNumber(it) }
-                .toSet()
-
-            fun create(vararg lottoNumbers: LottoNumber): LottoTicket = LottoTicket(lottoNumbers.toSet())
+        fun create(
+            lottoIssueType: LottoIssueType,
+            vararg lottoNumbers: Int,
+        ): LottoTicket {
+            return LottoTicket(lottoIssueType, lottoNumbers.map { LottoNumber(it) })
+        }
     }
 }
diff --git a/src/main/kotlin/lotto/model/LottoTicketCount.kt b/src/main/kotlin/lotto/model/LottoTicketCount.kt
new file mode 100644
index 000000000..1c9d317c0
--- /dev/null
+++ b/src/main/kotlin/lotto/model/LottoTicketCount.kt
@@ -0,0 +1,17 @@
+package lotto.model
+
+import lotto.contants.LottoRuleConstants
+
+class LottoTicketCount(
+    private val count: Int,
+) {
+    init {
+        require(count >= LottoRuleConstants.ZERO.value) { LOTTO_TICKET_COUNT_NOT_NEGATIVE }
+    }
+
+    fun toInt() = count
+
+    companion object {
+        private const val LOTTO_TICKET_COUNT_NOT_NEGATIVE = "로또 구매 장 수는 음수가 될수 없습니다."
+    }
+}
diff --git a/src/main/kotlin/lotto/model/LottoTicketGenerator.kt b/src/main/kotlin/lotto/model/LottoTicketGenerator.kt
new file mode 100644
index 000000000..24d451751
--- /dev/null
+++ b/src/main/kotlin/lotto/model/LottoTicketGenerator.kt
@@ -0,0 +1,7 @@
+package lotto.model
+
+interface LottoTicketGenerator {
+    val type: LottoIssueType
+
+    fun generateLottoTicket(): LottoTicket
+}
diff --git a/src/main/kotlin/lotto/model/LottoTicketIssueManager.kt b/src/main/kotlin/lotto/model/LottoTicketIssueManager.kt
new file mode 100644
index 000000000..0e254297e
--- /dev/null
+++ b/src/main/kotlin/lotto/model/LottoTicketIssueManager.kt
@@ -0,0 +1,25 @@
+package lotto.model
+
+class LottoTicketIssueManager(
+    totalCount: Int,
+    manualCount: Int,
+    manualLottoNumbers: List<List<Int>>,
+) {
+    private val autoLottoTicketCount =
+        LottoTicketCount(totalCount - manualCount)
+
+    init {
+        require(totalCount >= manualCount) { EXCEEDED_MANUAL_LOTTO_COUNT }
+        require(manualCount == manualLottoNumbers.size) { LOTTO_COUNT_NOT_MATCH_LOTTO_NUMBERS }
+    }
+
+    fun getLottoTickets(manualLottoNumbers: List<List<Int>>): List<LottoTicket> =
+        LottoMachine().issueLottoTickets(autoLottoTicketCount, manualLottoNumbers)
+
+    fun getAutoLottoTicketCount(): Int = autoLottoTicketCount.toInt()
+
+    companion object {
+        private const val LOTTO_COUNT_NOT_MATCH_LOTTO_NUMBERS = "수동 구매 개수와 수동 번호 입력 개수가 일치하지 않습니다."
+        private const val EXCEEDED_MANUAL_LOTTO_COUNT = "전체 발행 가능한 로또 개수보다 수동로또 발행 개수가 더 많습니다."
+    }
+}
diff --git a/src/main/kotlin/lotto/model/ManualLottoTicketGenerator.kt b/src/main/kotlin/lotto/model/ManualLottoTicketGenerator.kt
new file mode 100644
index 000000000..a4e10f376
--- /dev/null
+++ b/src/main/kotlin/lotto/model/ManualLottoTicketGenerator.kt
@@ -0,0 +1,9 @@
+package lotto.model
+
+class ManualLottoTicketGenerator(
+    private val numbers: List<Int>,
+) : LottoTicketGenerator {
+    override val type: LottoIssueType = LottoIssueType.MANUAL
+
+    override fun generateLottoTicket(): LottoTicket = LottoTicket(LottoIssueType.MANUAL, numbers.map(::LottoNumber))
+}
diff --git a/src/main/kotlin/lotto/model/WinningLotto.kt b/src/main/kotlin/lotto/model/WinningLotto.kt
index 231c645ee..8c8531322 100644
--- a/src/main/kotlin/lotto/model/WinningLotto.kt
+++ b/src/main/kotlin/lotto/model/WinningLotto.kt
@@ -1,37 +1,34 @@
 package lotto.model
 
-import lotto.contants.LottoRuleConstants.LOTTO_PICK_COUNT
-
 class WinningLotto(
-    private val winningNumbers: Set<LottoNumber>,
+    private val winningLottoTicket: LottoTicket,
     private val bonusNumber: LottoNumber,
 ) {
     init {
-        require(winningNumbers.size == LOTTO_PICK_COUNT.value) { ERROR_NUMBERS_COUNT }
-        require(!winningNumbers.contains(bonusNumber)) { ERROR_LOTTO_NUMBERS_NOT_CONTAIN_BONUS_NUMBER }
+        require(!winningLottoTicket.containsNumber(bonusNumber)) { ERROR_LOTTO_NUMBERS_NOT_CONTAIN_BONUS_NUMBER }
     }
 
-    fun getResult(lottoTickets: List<LottoTicket>): LottoResult {
-        val result = lottoTickets.map { getRank(it) }
-        return LottoResult(result)
-    }
+    fun getRanks(lottoTickets: List<LottoTicket>): List<Rank> = lottoTickets.map { getRank(it) }
 
-    fun getRank(lottoTicket: LottoTicket): Rank {
+    private fun getRank(lottoTicket: LottoTicket): Rank {
         val countOfMatch: Int = getCountOfMatch(lottoTicket)
         val matchBonus: Boolean = getMatchBonus(lottoTicket)
         return Rank.valueOf(countOfMatch, matchBonus)
     }
 
-    private fun getCountOfMatch(lottoTicket: LottoTicket): Int = winningNumbers.intersect(lottoTicket.getNumbers()).size
+    private fun getCountOfMatch(lottoTicket: LottoTicket): Int = winningLottoTicket.matchNumbersSize(lottoTicket)
 
-    private fun getMatchBonus(lottoTicket: LottoTicket): Boolean = lottoTicket.getNumbers().contains(bonusNumber)
+    private fun getMatchBonus(lottoTicket: LottoTicket): Boolean = lottoTicket.containsNumber(bonusNumber)
 
     companion object {
-        private const val ERROR_NUMBERS_COUNT = "로또 번호의 개수는 6개입니다."
         private const val ERROR_LOTTO_NUMBERS_NOT_CONTAIN_BONUS_NUMBER = "로또 당첨 번호는 보너스 번호를 포함하지 말아야합니다."
 
-        fun create(vararg lottoNumbers: LottoNumber, bonusNumber: LottoNumber): WinningLotto {
-            return WinningLotto(lottoNumbers.toSet(), bonusNumber)
+        fun create(
+            vararg lottoNumbers: Int,
+            bonusNumber: Int,
+        ): WinningLotto {
+            val winningNumbers = LottoTicket(LottoIssueType.WINNING, lottoNumbers.map { LottoNumber(it) })
+            return WinningLotto(winningNumbers, LottoNumber(bonusNumber))
         }
     }
 }
diff --git a/src/main/kotlin/lotto/view/InputValidator.kt b/src/main/kotlin/lotto/view/InputValidator.kt
index bb4fea044..9377ed425 100644
--- a/src/main/kotlin/lotto/view/InputValidator.kt
+++ b/src/main/kotlin/lotto/view/InputValidator.kt
@@ -5,18 +5,14 @@ import lotto.contants.LottoRuleConstants
 class InputValidator {
     fun validateInteger(input: String) = require(input.toIntOrNull() != null) { ERROR_INVALID_INTEGER }
 
-    fun validateOverZero(input: String) = require(input.toInt() > LottoRuleConstants.ZERO.value) { ERROR_INVALID_OVER_ZERO }
+    fun validateOverZero(input: String) =
+        require(input.toInt() > LottoRuleConstants.ZERO.value) { ERROR_INVALID_OVER_ZERO }
 
-    fun validateAmountUnits1000(input: String) =
-        require(
-            input.toInt() % LottoRuleConstants.LOTTO_AMOUNT.value == LottoRuleConstants.ZERO.value,
-        ) {
-            ERROR_INVALID_DIVISION
-        }
+    fun validateNoNegativeNumber(input: String) =
+        require(input.toInt() >= LottoRuleConstants.ZERO.value) { ERROR_INVALID_OVER_ZERO }
 
     companion object {
         private const val ERROR_INVALID_INTEGER = "정수를 입력해야 합니다."
         private const val ERROR_INVALID_OVER_ZERO = "구입금액은 0보다 커야 합니다."
-        private const val ERROR_INVALID_DIVISION = "구입금액은 1000의 배수여야 합니다."
     }
 }
diff --git a/src/main/kotlin/lotto/view/InputView.kt b/src/main/kotlin/lotto/view/InputView.kt
deleted file mode 100644
index 31c820aa7..000000000
--- a/src/main/kotlin/lotto/view/InputView.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package lotto.view
-
-object InputView {
-    fun getUserInput(): String = readln().trim()
-}
diff --git a/src/main/kotlin/lotto/view/OutputView.kt b/src/main/kotlin/lotto/view/OutputView.kt
deleted file mode 100644
index 1f6c474a5..000000000
--- a/src/main/kotlin/lotto/view/OutputView.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package lotto.view
-
-object OutputView {
-    fun printMessage(msg: Any) = println(msg)
-}
diff --git a/src/main/kotlin/lotto/view/UserInterface.kt b/src/main/kotlin/lotto/view/UserInterface.kt
deleted file mode 100644
index 231fae810..000000000
--- a/src/main/kotlin/lotto/view/UserInterface.kt
+++ /dev/null
@@ -1,76 +0,0 @@
-package lotto.view
-
-import lotto.model.LottoNumber
-import lotto.model.LottoTicket
-import lotto.model.Rank
-import java.text.DecimalFormat
-
-class UserInterface(
-    private val inputValidator: InputValidator = InputValidator(),
-) {
-    fun inputPurchaseAmount(): Int {
-        OutputView.printMessage("구입금액을 입력해 주세요.")
-        val purchaseAmount = InputView.getUserInput()
-        inputValidator.validateInteger(purchaseAmount)
-        inputValidator.validateOverZero(purchaseAmount)
-        inputValidator.validateAmountUnits1000(purchaseAmount)
-        return purchaseAmount.toInt()
-    }
-
-    fun printLottoTickets(lottoTickets: List<LottoTicket>) {
-        OutputView.printMessage("${lottoTickets.size}개를 구매했습니다.")
-        lottoTickets.map {
-            OutputView.printMessage(it.getNumbers().map { it.number }.joinToString(", ", "[", "]"))
-        }
-    }
-
-    fun getWinningNumbers(): Set<LottoNumber> {
-        OutputView.printMessage("\n지난 주 당첨 번호를 입력해 주세요.")
-        val winningNumbers = InputView.getUserInput().split(",").map { it.trim() }
-        for (winningNumber in winningNumbers) {
-            inputValidator.validateInteger(winningNumber)
-        }
-        return winningNumbers.map { LottoNumber(it.toInt()) }.toSet()
-    }
-
-    fun getBonusNumber(): LottoNumber {
-        OutputView.printMessage("보너스 볼을 입력해 주세요.")
-        val bonusNumber = InputView.getUserInput()
-        inputValidator.validateInteger(bonusNumber)
-        return LottoNumber(bonusNumber.toInt())
-    }
-
-    fun printResult(results: Map<Rank, Int>) {
-        println("\n당첨 통계")
-        println("---------")
-
-        val rankOrder = listOf(Rank.FIFTH, Rank.FOURTH, Rank.THIRD, Rank.SECOND, Rank.FIRST)
-
-        val rankMessages =
-            mapOf(
-                Rank.FIFTH to "3개 일치",
-                Rank.FOURTH to "4개 일치",
-                Rank.THIRD to "5개 일치",
-                Rank.SECOND to "5개 일치, 보너스 볼 일치",
-                Rank.FIRST to "6개 일치",
-            )
-
-        val prizeMessages =
-            mapOf(
-                Rank.FIFTH to "5000원",
-                Rank.FOURTH to "50000원",
-                Rank.THIRD to "1500000원",
-                Rank.SECOND to "30000000원",
-                Rank.FIRST to "2000000000원",
-            )
-        for (rank in rankOrder) {
-            val count = results.getOrDefault(rank, 0)
-            println("${rankMessages[rank]} (${prizeMessages[rank]}) - ${count}개")
-        }
-    }
-
-    fun printProfit(profit: Double) {
-        val df = DecimalFormat("#.##")
-        OutputView.printMessage("총 수익률은 ${df.format(profit)}입니다.")
-    }
-}
diff --git a/src/main/kotlin/lotto/view/ViewFlow.kt b/src/main/kotlin/lotto/view/ViewFlow.kt
new file mode 100644
index 000000000..6d9941130
--- /dev/null
+++ b/src/main/kotlin/lotto/view/ViewFlow.kt
@@ -0,0 +1,159 @@
+package lotto.view
+
+import lotto.model.LottoNumber
+import lotto.model.LottoTicket
+import lotto.model.Rank
+import java.text.DecimalFormat
+
+class ViewFlow(
+    private val inputValidator: InputValidator = InputValidator(),
+) {
+    fun inputPurchaseAmount(): Int =
+        runCatching {
+            println(PURCHASE_AMOUNT_PROMPT)
+            val purchaseAmount = readln().trim()
+            inputValidator.validateInteger(purchaseAmount)
+            inputValidator.validateOverZero(purchaseAmount)
+            return purchaseAmount.toInt()
+        }.getOrElse { error ->
+            println(error)
+            inputPurchaseAmount()
+        }
+
+    fun printLottoCount(lottoCount: Int): Boolean =
+        runCatching {
+            println(String.format(LOTTO_COUNT_PROMPT, lottoCount, lottoCount))
+            val yesOrNo = readln().trim()
+            return yesOrNo == "Y"
+        }.getOrElse { error ->
+            println(error)
+            printLottoCount(lottoCount)
+        }
+
+    fun printChange(change: Int) {
+        println(String.format(CHANGE_, change))
+    }
+
+    fun getManualLottoCount(): Int =
+        runCatching {
+            println(MANUAL_LOTTO_COUNT_PROMPT)
+            val manualLottoCount = readln().trim()
+            inputValidator.validateInteger(manualLottoCount)
+            inputValidator.validateNoNegativeNumber(manualLottoCount)
+            return manualLottoCount.toInt()
+        }.getOrElse { error ->
+            println(error)
+            getManualLottoCount()
+        }
+
+    fun getManualLottoNumbers(manualLottoCount: Int): List<List<Int>> =
+        runCatching {
+            val manualLottoNumber = mutableListOf<List<Int>>()
+            println(MANUAL_LOTTO_NUMBERS_PROMPT)
+            println(MANUAL_LOTTO_FORMAT_PROMPT)
+            repeat(manualLottoCount) {
+                val numbers = readln().trim()
+                manualLottoNumber.add(numbers.split(",").map { it.trim().toInt() })
+            }
+            return manualLottoNumber
+        }.getOrElse { error ->
+            println(error)
+            getManualLottoNumbers(manualLottoCount)
+        }
+
+    fun printLottoTickets(
+        manualLottoCount: Int,
+        autoLottoCount: Int,
+        lottoTickets: List<LottoTicket>,
+    ) {
+        println(String.format(LOTTO_PURCHASE_RESULT_, manualLottoCount, autoLottoCount))
+        lottoTickets.map {
+            print("${it.lottoIssueType.issueType} ")
+            print(it.getNumbers().map { it.number }.joinToString(", ", "[", "]\n"))
+        }
+    }
+
+    fun getWinningNumbers(): List<LottoNumber> =
+        runCatching {
+            println(WINNING_NUMBERS_PROMPT)
+            val winningNumbers = readln().trim().split(",").map { it.trim() }
+            for (winningNumber in winningNumbers) {
+                inputValidator.validateInteger(winningNumber)
+            }
+
+            return winningNumbers.map { LottoNumber(it.toInt()) }
+        }.getOrElse { error ->
+            println(error)
+            getWinningNumbers()
+        }
+
+    fun getBonusNumber(): LottoNumber =
+        runCatching {
+            println(BONUS_NUMBER_PROMPT)
+            val bonusNumber = readln().trim()
+            inputValidator.validateInteger(bonusNumber)
+            return LottoNumber(bonusNumber.toInt())
+        }.getOrElse { error ->
+            println(error)
+            getBonusNumber()
+        }
+
+    fun printResult(results: Map<Rank, Int>) {
+        println(WINNING_STATISTICS_HEADER)
+        println(WINNING_STATISTICS_DIVIDER)
+
+        val rankOrder = listOf(Rank.FIFTH, Rank.FOURTH, Rank.THIRD, Rank.SECOND, Rank.FIRST)
+
+        val ranks =
+            mapOf(
+                Rank.FIFTH to RANK_3_MATCH,
+                Rank.FOURTH to RANK_4_MATCH,
+                Rank.THIRD to RANK_5_MATCH,
+                Rank.SECOND to RANK_5_MATCH_BONUS,
+                Rank.FIRST to RANK_6_MATCH,
+            )
+
+        val prizes =
+            mapOf(
+                Rank.FIFTH to PRIZE_5000,
+                Rank.FOURTH to PRIZE_50000,
+                Rank.THIRD to PRIZE_1500000,
+                Rank.SECOND to PRIZE_30000000,
+                Rank.FIRST to PRIZE_2000000000,
+            )
+        for (rank in rankOrder) {
+            val count = results.getOrDefault(rank, 0)
+            println("${ranks[rank]} (${prizes[rank]}) - ${count}개")
+        }
+    }
+
+    fun printProfit(profit: Double) {
+        val df = DecimalFormat("#.##")
+        println(String.format(PROFIT_, df.format(profit)))
+    }
+
+    companion object {
+        private const val PURCHASE_AMOUNT_PROMPT = "구입금액을 입력해 주세요."
+        private const val LOTTO_COUNT_PROMPT = "\n발행할 수 있는 로또 수는 %d입니다. %d만큼 발행하겠습니까? Y / N 로 대답해주세요."
+        private const val CHANGE_ = "\n거스름 돈은 %d원입니다."
+        private const val MANUAL_LOTTO_COUNT_PROMPT = "\n수동으로 구매할 로또 수를 입력해 주세요."
+        private const val MANUAL_LOTTO_NUMBERS_PROMPT = "\n수동으로 구매할 번호를 입력해 주세요."
+        private const val MANUAL_LOTTO_FORMAT_PROMPT = "수동으로 구매할 번호는 쉼표로 구분된 숫자로 입력해 주세요."
+        private const val WINNING_NUMBERS_PROMPT = "\n지난 주 당첨 번호를 입력해 주세요."
+        private const val BONUS_NUMBER_PROMPT = "보너스 볼을 입력해 주세요."
+        private const val LOTTO_PURCHASE_RESULT_ = "\n수동으로 %d장, 자동으로 %d개를 구매했습니다."
+        private const val WINNING_STATISTICS_HEADER = "\n당첨 통계"
+        private const val WINNING_STATISTICS_DIVIDER = "---------"
+        private const val PRIZE_5000 = "5000원"
+        private const val PRIZE_50000 = "50000원"
+        private const val PRIZE_1500000 = "1500000원"
+        private const val PRIZE_30000000 = "30000000원"
+        private const val PRIZE_2000000000 = "2000000000원"
+        private const val RANK_3_MATCH = "3개 일치"
+        private const val RANK_4_MATCH = "4개 일치"
+        private const val RANK_5_MATCH = "5개 일치"
+        private const val RANK_5_MATCH_BONUS = "5개 일치, 보너스 볼 일치"
+        private const val RANK_6_MATCH = "6개 일치"
+        private const val PROFIT_ = "총 수익률은 %s입니다."
+    }
+}
diff --git a/src/test/kotlin/lotto/model/AutoLottoTicketGeneratorTest.kt b/src/test/kotlin/lotto/model/AutoLottoTicketGeneratorTest.kt
new file mode 100644
index 000000000..5592c4cd8
--- /dev/null
+++ b/src/test/kotlin/lotto/model/AutoLottoTicketGeneratorTest.kt
@@ -0,0 +1,15 @@
+package lotto.model
+
+import org.assertj.core.api.Assertions
+import org.junit.jupiter.api.Test
+
+class AutoLottoTicketGeneratorTest {
+    @Test
+    fun `로또 자동 발행하면, 자동 로또 티켓을 반환한다`() {
+        val actual = AutoLottoTicketGenerator().generateLottoTicket().lottoIssueType
+
+        val expected = LottoIssueType.AUTO
+
+        Assertions.assertThat(actual).isEqualTo(expected)
+    }
+}
\ No newline at end of file
diff --git a/src/test/kotlin/lotto/model/LottoMachineTest.kt b/src/test/kotlin/lotto/model/LottoMachineTest.kt
index 104269db0..4da6fe09f 100644
--- a/src/test/kotlin/lotto/model/LottoMachineTest.kt
+++ b/src/test/kotlin/lotto/model/LottoMachineTest.kt
@@ -1,14 +1,20 @@
 package lotto.model
 
-import org.junit.jupiter.api.Assertions.assertEquals
-import org.junit.jupiter.params.ParameterizedTest
-import org.junit.jupiter.params.provider.ValueSource
+import org.assertj.core.api.Assertions
+import org.junit.jupiter.api.Assertions.*
+import org.junit.jupiter.api.Test
 
 class LottoMachineTest {
-    @ParameterizedTest
-    @ValueSource(ints = [1, 3, 4])
-    fun `로또 구입 개수만큼 로또를 발행한다`(count: Int) {
-        val lottoMachine = LottoMachine()
-        assertEquals(lottoMachine.purchase(count).size, count)
+    @Test
+    fun `로또 발행기는 총 로또 구매 개수만큼 로또 티켓을 생성한다`() {
+        val autoLottoTicketCount = LottoTicketCount(1)
+        val manualLottoNumbers = emptyList<List<Int>>()
+        val lottoTickets = LottoMachine().issueLottoTickets(autoLottoTicketCount, manualLottoNumbers)
+
+        val actual = lottoTickets.size
+
+        val expected = 1
+
+        Assertions.assertThat(actual).isEqualTo(expected)
     }
-}
+}
\ No newline at end of file
diff --git a/src/test/kotlin/lotto/model/LottoStoreCashierTest.kt b/src/test/kotlin/lotto/model/LottoStoreCashierTest.kt
new file mode 100644
index 000000000..d9644b258
--- /dev/null
+++ b/src/test/kotlin/lotto/model/LottoStoreCashierTest.kt
@@ -0,0 +1,40 @@
+package lotto.model
+
+import io.kotest.assertions.Expected
+import org.assertj.core.api.Assertions
+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 LottoStoreCashierTest {
+    @Test
+    fun `구입하는 돈이 1000원 보다 작으면 에러를 반환한다`() {
+        assertThrows<IllegalArgumentException> {
+            LottoStoreCashier(900)
+        }
+    }
+
+    @ParameterizedTest
+    @CsvSource(value = ["1000, 1"])
+    fun `돈을 주면 구입가능한 수량을 반환한다`(
+        money: Int,
+        expected: Int,
+    ) {
+        val actual = LottoStoreCashier(money).calculatePossibleToBuyLottoTicketCount()
+
+        Assertions.assertThat(actual).isEqualTo(expected)
+    }
+
+    @ParameterizedTest
+    @CsvSource(value = ["1000, 1, 0", "1500, 1, 500"])
+    fun `돈을 주면 고객이 구입을 원하는 수량외 잔액을 계산해 반환한다`(
+        money: Int,
+        count: Int,
+        expected: Int,
+    ) {
+        val actual = LottoStoreCashier(money).calculateChange(count)
+
+        Assertions.assertThat(actual).isEqualTo(expected)
+    }
+}
\ No newline at end of file
diff --git a/src/test/kotlin/lotto/model/LottoTest.kt b/src/test/kotlin/lotto/model/LottoTest.kt
index f706321bb..1413b63cd 100644
--- a/src/test/kotlin/lotto/model/LottoTest.kt
+++ b/src/test/kotlin/lotto/model/LottoTest.kt
@@ -7,23 +7,14 @@ import org.junit.jupiter.api.assertThrows
 class LottoTest {
     @Test
     fun `로또는 한 장에 로또 번호 6개를 가진다`() {
-        val lotto = LottoTicket()
+        val lotto = LottoTicket.create(LottoIssueType.MANUAL, 1, 2, 3, 4, 5, 6)
         assertEquals(lotto.getSize(), 6)
     }
 
     @Test
-    fun `로또 번호는 서로 중복되면 에러가 발생한다`() {
+    fun `로또 번호는 서로 중복된 수가 주어지면 에러가 발생한다`() {
         assertThrows<IllegalArgumentException> {
-            LottoTicket(
-                setOf(
-                    LottoNumber(1),
-                    LottoNumber(2),
-                    LottoNumber(3),
-                    LottoNumber(4),
-                    LottoNumber(5),
-                    LottoNumber(5),
-                ),
-            )
+            LottoTicket.create(LottoIssueType.MANUAL, 2, 3, 4, 5, 5)
         }
     }
 }
diff --git a/src/test/kotlin/lotto/model/LottoTicketCountTest.kt b/src/test/kotlin/lotto/model/LottoTicketCountTest.kt
new file mode 100644
index 000000000..6bbcaf3db
--- /dev/null
+++ b/src/test/kotlin/lotto/model/LottoTicketCountTest.kt
@@ -0,0 +1,15 @@
+package lotto.model
+
+import org.junit.jupiter.api.assertThrows
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ValueSource
+
+class LottoTicketCountTest {
+    @ParameterizedTest
+    @ValueSource(ints = [-1])
+    fun `로또 구매 장수에 음수가 들어오면 에러가 발생한다`(count: Int) {
+        assertThrows<IllegalArgumentException> {
+            LottoTicketCount(count)
+        }
+    }
+}
diff --git a/src/test/kotlin/lotto/model/LottoTicketIssueManagerTest.kt b/src/test/kotlin/lotto/model/LottoTicketIssueManagerTest.kt
new file mode 100644
index 000000000..8b657bc5e
--- /dev/null
+++ b/src/test/kotlin/lotto/model/LottoTicketIssueManagerTest.kt
@@ -0,0 +1,50 @@
+package lotto.model
+
+import org.assertj.core.api.Assertions
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+
+class LottoTicketIssueManagerTest {
+    @Test
+    fun `수동 구매 개수와 수동 번호 입력 개수가 일치하지 않으면 에러를 발생한다`() {
+        val manualLottoCount = 0
+        val manualLottoNumbers = listOf(listOf(1, 2, 3, 4, 5, 6))
+        val totalCount = 1
+
+        assertThrows<IllegalArgumentException> {
+            LottoTicketIssueManager(
+                totalCount,
+                manualLottoCount,
+                manualLottoNumbers,
+            )
+        }
+    }
+
+    @Test
+    fun `수동 구매가 총 구매 개수를 초과하면 에러가 난다`() {
+        val manualLottoCount = 2
+        val manualLottoNumbers = listOf(listOf(1, 2, 3, 4, 5, 6))
+        val totalCount = 1
+
+        assertThrows<IllegalArgumentException> {
+            LottoTicketIssueManager(
+                totalCount,
+                manualLottoCount,
+                manualLottoNumbers,
+            )
+        }
+    }
+
+    @Test
+    fun `자동 로또 구매 개수를 반환한다`() {
+        val manualLottoCount = 1
+        val manualLottoNumbers = listOf(listOf(1, 2, 3, 4, 5, 6))
+        val totalCount = 2
+
+        val actual = LottoTicketIssueManager(totalCount, manualLottoCount, manualLottoNumbers).getAutoLottoTicketCount()
+
+        val expected = 1
+
+        Assertions.assertThat(actual).isEqualTo(expected)
+    }
+}
diff --git a/src/test/kotlin/lotto/model/LottoTicketTest.kt b/src/test/kotlin/lotto/model/LottoTicketTest.kt
new file mode 100644
index 000000000..4c266820f
--- /dev/null
+++ b/src/test/kotlin/lotto/model/LottoTicketTest.kt
@@ -0,0 +1,61 @@
+package lotto.model
+
+import org.assertj.core.api.Assertions
+import org.junit.jupiter.api.Test
+
+import org.junit.jupiter.api.Assertions.*
+
+class LottoTicketTest {
+
+    @Test
+    fun `로또 티켓의 숫자가 6개가 아니면, 에러를 반환한다`() {
+        org.junit.jupiter.api.assertThrows<IllegalArgumentException> {
+            LottoTicket.create(LottoIssueType.MANUAL, 1, 2, 3, 4, 5)
+        }
+    }
+
+    @Test
+    fun `로또 티켓의 번호에 번호가 포함되어있으면 true를 반환한다`() {
+        val lottoTicket = LottoTicket.create(LottoIssueType.MANUAL, 1, 2, 3, 4, 5, 6)
+
+        val actual = lottoTicket.containsNumber(LottoNumber(1))
+
+        val expected = true
+
+        Assertions.assertThat(actual).isEqualTo(expected)
+    }
+
+    @Test
+    fun `로또 티켓의 번호에 번호가 포함되어있지 않으면 false를 반환한다`() {
+        val lottoTicket = LottoTicket.create(LottoIssueType.MANUAL, 1, 2, 3, 4, 5, 6)
+
+        val actual = lottoTicket.containsNumber(LottoNumber(7))
+
+        val expected = false
+
+        Assertions.assertThat(actual).isEqualTo(expected)
+    }
+
+    @Test
+    fun `로또 티켓의 번호를 반환한다`() {
+        val lottoTicket = LottoTicket.create(LottoIssueType.MANUAL, 1, 2, 3, 4, 5, 6)
+
+        val actual = lottoTicket.getNumbers()
+
+        val expected =
+            setOf(LottoNumber(1), LottoNumber(2), LottoNumber(3), LottoNumber(4), LottoNumber(5), LottoNumber(6))
+
+        Assertions.assertThat(actual).isEqualTo(expected)
+    }
+
+    @Test
+    fun `로또 번호가 주어지면 일치하는 번호의 개수를 반환한다`() {
+        val lottoTicket1 = LottoTicket.create(LottoIssueType.MANUAL, 1, 2, 3, 4, 5, 6)
+        val lottoTicket2 = LottoTicket.create(LottoIssueType.MANUAL, 1, 2, 3, 4, 5, 6)
+        val actual = lottoTicket1.matchNumbersSize(lottoTicket2)
+
+        val expected = 6
+
+        Assertions.assertThat(actual).isEqualTo(expected)
+    }
+}
\ No newline at end of file
diff --git a/src/test/kotlin/lotto/model/ManualLottoTicketGeneratorTest.kt b/src/test/kotlin/lotto/model/ManualLottoTicketGeneratorTest.kt
new file mode 100644
index 000000000..69c2906d1
--- /dev/null
+++ b/src/test/kotlin/lotto/model/ManualLottoTicketGeneratorTest.kt
@@ -0,0 +1,30 @@
+package lotto.model
+
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Assertions.*
+import org.junit.jupiter.api.Test
+
+class ManualLottoTicketGeneratorTest {
+    @Test
+    fun `수동로또의 타입은 수동이다`() {
+        val numbers = listOf(1, 2, 3, 4, 5, 6)
+        val manualLotto = ManualLottoTicketGenerator(numbers)
+
+        val actual = manualLotto.type
+
+        val expected = LottoIssueType.MANUAL
+
+        assertThat(actual).isEqualTo(expected)
+    }
+
+    @Test
+    fun `수동로또 생성기는 수동 로또를 반환한다`() {
+        val numbers = listOf(1, 2, 3, 4, 5, 6)
+
+        val actual = ManualLottoTicketGenerator(numbers).generateLottoTicket()
+
+        val expected = LottoTicket.create(LottoIssueType.MANUAL, 1, 2, 3, 4, 5, 6)
+
+        assertThat(actual).isEqualTo(expected)
+    }
+}
diff --git a/src/test/kotlin/lotto/model/RankTest.kt b/src/test/kotlin/lotto/model/RankTest.kt
new file mode 100644
index 000000000..b69eed615
--- /dev/null
+++ b/src/test/kotlin/lotto/model/RankTest.kt
@@ -0,0 +1,19 @@
+package lotto.model
+
+import org.assertj.core.api.Assertions
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.CsvSource
+
+class RankTest {
+    @ParameterizedTest
+    @CsvSource(value = ["6, false, FIRST", "5, true, SECOND", "5, false, THIRD", "4, false, FOURTH", "3, false, FIFTH"])
+    fun `일치하는 숫자와 보너스 매치 여부가 들어오면 rank를 반환한다`(
+        countOfMatch: Int,
+        matchBonus: Boolean,
+        expected: Rank,
+    ) {
+        val actual = Rank.valueOf(countOfMatch, matchBonus)
+
+        Assertions.assertThat(actual).isEqualTo(expected)
+    }
+}
diff --git a/src/test/kotlin/lotto/model/WinningLottoTest.kt b/src/test/kotlin/lotto/model/WinningLottoTest.kt
index 6341b610c..8b08c96c5 100644
--- a/src/test/kotlin/lotto/model/WinningLottoTest.kt
+++ b/src/test/kotlin/lotto/model/WinningLottoTest.kt
@@ -6,30 +6,9 @@ import org.junit.jupiter.api.Test
 class WinningLottoTest {
     @Test
     fun `로또 당첨을 판단한다`() {
-        val bonusNumber = LottoNumber(7)
-
-        val winningLotto =
-            WinningLotto.create(
-                    LottoNumber(1),
-                    LottoNumber(2),
-                    LottoNumber(3),
-                    LottoNumber(4),
-                    LottoNumber(5),
-                    LottoNumber(6),
-                bonusNumber = bonusNumber,
-            )
-
-        val lottoTicket =
-            LottoTicket.create(
-                    LottoNumber(1),
-                    LottoNumber(2),
-                    LottoNumber(3),
-                    LottoNumber(4),
-                    LottoNumber(5),
-                    LottoNumber(6),
-            )
-
-        val rank = winningLotto.getRank(lottoTicket)
+        val winningLotto = WinningLotto.create(1, 2, 3, 4, 5, 6, bonusNumber = 7)
+        val lottoTicket = LottoTicket.create(LottoIssueType.MANUAL, 1, 2, 3, 4, 5, 6)
+        val rank = winningLotto.getRanks(listOf(lottoTicket))[0]
 
         assertThat(rank).isEqualTo(Rank.FIRST)
     }
diff --git a/src/test/kotlin/lotto/view/InputTest.kt b/src/test/kotlin/lotto/view/InputTest.kt
index 5a39e6748..1c7baa561 100644
--- a/src/test/kotlin/lotto/view/InputTest.kt
+++ b/src/test/kotlin/lotto/view/InputTest.kt
@@ -28,12 +28,4 @@ class InputTest {
             InputValidator().validateOverZero(input)
         }
     }
-
-    @ParameterizedTest
-    @ValueSource(strings = ["500", "1234"])
-    fun `구입금액이 1000으로 나누어 떨어지지 않으면 예외를 발생시킨다`(input: String) {
-        assertThrows<IllegalArgumentException> {
-            InputValidator().validateAmountUnits1000(input)
-        }
-    }
 }


Reviews:
user: login: Gyuil-Hwnag
body: 모찌 안녕하세요!
해당 미션 구현 고생 많으셨습니다!

이번 단계를 진행하면서 많이 어려움이 있으셨나 보군요..🥲
구현한 내용들을 보면서, 이번 미션을 진행하면서 어려움이 있으셨는지 구현에 통일성이 없다는 생각이 들었어요
어떤 부분에서 어려움이 있었는지 같이 의논해가면서 해결해나가면 되는거라서, 의논해보고 싶은 내용있다면 언제든 말씀 주세요~
고민해보시면 좋을만한 내용들 코멘트를 달았으니 확인해주시면 감사하겠습니다.

궁금한 점이나 논의하고 싶은 내용이 있으면 언제든 DM 주세요! 🙏
화이팅입니다💪
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-02-28T02:37:30Z
user: login: wondroid-world
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-03T04:09:49Z
user: login: wondroid-world
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-03T04:19:05Z
user: login: wondroid-world
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-03T04:31:14Z
user: login: wondroid-world
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-03T04:40:31Z
user: login: wondroid-world
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-03T04:52:29Z
user: login: wondroid-world
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-03T05:05:08Z
user: login: wondroid-world
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-03T06:23:56Z
user: login: wondroid-world
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-03T06:46:03Z
user: login: Gyuil-Hwnag
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-03T14:16:42Z
user: login: Gyuil-Hwnag
body: 모찌 안녕하세요!

해당 미션 리뷰 반영 고생 많으셨습니다
리뷰 반영하면서 구현한 내용 수정 너무 잘 해주셨습니다 💯

고민해보시면 좋을만한 내용들 코멘트를 달았으니 확인해주시면 감사하겠습니다.
다음 미션인 블랙잭 미션이 로직적으로 많은 어려움이 있어서, 모든 코멘트를 반영하기 보다는 `UserInterface (Input / Output) / 테스트 코드` 부분만 이라도 우선적으로 살펴보고 나머지 코멘트는 살펴보고 고민후 추후 적용을 해봐도 좋을 것 같다는 생각이 듭니다!

위에 부분은 블랙잭 미션전에 한번 정리를 하고 가면 좋을 것 같다는 생각이 듭니다~
다만 다음 미션에서 시간이 많이 소요될 것 같아서, 반영에 시간이 오래걸리실 것 같다면 코멘트에 대한 의견이라도 남겨주신다음에 다시 요청 주셔도 좋습니다

궁금한 점이나 논의하고 싶은 내용이 있으면 언제든 DM 주세요! 🙏
화이팅입니다💪
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-03T14:45:05Z
user: login: wondroid-world
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-08T05:25:32Z
user: login: wondroid-world
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-08T05:29:50Z
user: login: Gyuil-Hwnag
body: 모찌 안녕하세요!
미션 정말 고생 많으셨습니다! 👏

현재 블랙잭 미션을 진행하시느라 많이 바쁘실텐데, 이전 미션인 로또 미션의 마무리 잘해주셨네요
정말 고생 많으셨습니다!

이번 미션을 진행하면서, Domain / View에 대해서 얘기를 나눠보고 각 객체의 책임을 분리하는 것을 해보았는데, 다음 미션들을 진행하면서 많은 고민을 하고 거기서 많은 것들을 얻어서 적용을 한다면 앞으로 더욱더 발전하시리라 생각이 듭니다.
또한 구현을 하시면서 여러가지 고민도 많이 하시고 나왔던 키워드들이 조금씩 있었는데, 해당 내용들 잘 적용해서 다음미션도 잘 진행 하시리라 믿고 응원하겠습니다😆

궁금한 점이나 논의하고 싶은 내용이 있으면 언제든 DM 주세요! 🙏
앞으로도 응원하겠습니다 💪
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-08T14:36:17Z

Review comments:
diff_hunk: @@ -0,0 +1,16 @@
+package lotto.model
+
+import lotto.contants.LottoRuleConstants
+
+class AutoLottoTicketGenerator: LottoTicketGenerator {
+    override val type: LottoIssueType = LottoIssueType.AUTO
+
+    override fun generateLottoTicket(): LottoTicket {
+        val lottoNumbers = (LottoRuleConstants.MINIMUM_NUMBER.value..LottoRuleConstants.MAXIMUM_NUMBER.value)
+            .shuffled()
+            .take(LottoRuleConstants.LOTTO_PICK_COUNT.value)
+            .sorted()
+            .map(::LottoNumber)
+        return LottoTicket(LottoIssueType.AUTO, lottoNumbers)
+    }
+}
path: src/main/kotlin/lotto/model/AutoLottoTicketGenerator.kt
user: login: Gyuil-Hwnag
body: 다음 설정 옵션을 활성화하여 EOF 경고를 없애주세요🥲
File > Preferences > Editor > General > Ensure every saved file ends with a line break

이전에는 없다가 새로 생긴걸 보니, Android Studio가 재설치 되었거나 했나 보군요~
created_at: 2025-02-28T02:14:59Z
author_association: MEMBER
diff_hunk: @@ -7,23 +7,14 @@ import org.junit.jupiter.api.assertThrows
 class LottoTest {
     @Test
path: src/test/kotlin/lotto/model/LottoTest.kt
user: login: Gyuil-Hwnag
body: 추가로 미션 제출전에 다음 설정으로 문법에 안 맞는 내용은 없는지 확인해 보시는 것도 좋을것 같습니다~😆
`./gradlew clean ktlintCheck`

<img width="500" alt="스크린샷 2025-02-28 오전 11 16 24" src="https://github.com/user-attachments/assets/7e3106cf-a6da-414c-a6de-5f2e6f610ff5" />

created_at: 2025-02-28T02:17:07Z
author_association: MEMBER
diff_hunk: @@ -1,39 +1,57 @@
 package lotto.controller
 
-import lotto.contants.LottoRuleConstants
-import lotto.model.LottoMachine
-import lotto.model.LottoResult
-import lotto.model.LottoTicket
-import lotto.model.WinningLotto
+import lotto.model.*
 import lotto.view.UserInterface
 
 class LottoController(
     private val userInterface: UserInterface = UserInterface(),
 ) {
-    fun run() {
-        val lottoTickets = generateLottoTickets()
-        val winningLotto = getWinningLotto()
-        val lottoResult = winningLotto.getResult(lottoTickets)
-        getResult(lottoResult)
+    fun run(): Result<Unit> {
+        return kotlin.runCatching {
+            val possibleToLottoTicketCount = meetLottoStoreCashier()
+            val lottoTickets = getLottoTickets(possibleToLottoTicketCount)
+            val winningLotto = getWinningLotto()
+            getResult(lottoTickets, winningLotto)
+        }.map {
+            Result.success(Unit)
+        }.getOrElse {
+            e ->  Result.failure(Exception("(로또 실행 중 오류 발생) : ${e.message}"))
+        }
path: src/main/kotlin/lotto/controller/LottoController.kt
user: login: Gyuil-Hwnag
body: 현재와 같은 구조가 나오게 된 이유가 궁긍합니다!🤔
created_at: 2025-02-28T02:17:53Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,16 @@
+package lotto.model
+
+import lotto.contants.LottoRuleConstants
+
+class AutoLottoTicketGenerator: LottoTicketGenerator {
+    override val type: LottoIssueType = LottoIssueType.AUTO
+
+    override fun generateLottoTicket(): LottoTicket {
+        val lottoNumbers = (LottoRuleConstants.MINIMUM_NUMBER.value..LottoRuleConstants.MAXIMUM_NUMBER.value)
+            .shuffled()
+            .take(LottoRuleConstants.LOTTO_PICK_COUNT.value)
+            .sorted()
+            .map(::LottoNumber)
+        return LottoTicket(LottoIssueType.AUTO, lottoNumbers)
+    }
path: src/main/kotlin/lotto/model/AutoLottoTicketGenerator.kt
user: login: Gyuil-Hwnag
body: 자동 로또 생성기 정말 좋네요~👍
다만, type이 꼭 있어야 하는 이유가 있을까요?
현재 LottoTicket 에서는 전혀 사용을 안하고 있는 것 같은데, 추가를 하신 이유가 궁급합니다🤔
created_at: 2025-02-28T02:20:01Z
author_association: MEMBER
diff_hunk: @@ -1,5 +1,25 @@
 package lotto.model
 
+// 로또 발행하는 머신
 class LottoMachine {
-    fun purchase(count: Int): List<LottoTicket> = List(count) { LottoTicket() }
+    // 로또 티켓을 발행한다.
+    fun issueLottoTickets(
+        customerWantToBuyAutoLottoTicketCount: LottoTicketCount,
+        manualLottoNumbers: List<List<Int>>,
+    ): List<LottoTicket> =
+        purchaseManualLottoTickets(manualLottoNumbers) + purchaseAutoLottoTickets(customerWantToBuyAutoLottoTicketCount)
path: src/main/kotlin/lotto/model/LottoMachine.kt
user: login: Gyuil-Hwnag
body: issue 라는 네이밍이 뭔가 와닿지를 않기는 했는데, create, make 등등이 있었을 것 같긴 한데 모찌는 어떠신가요..?🤔
created_at: 2025-02-28T02:22:26Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,24 @@
+package lotto.model
+
+import lotto.contants.LottoRuleConstants
+
+// 돈 계산을 해주는 클래스
+class LottoStoreCashier(
path: src/main/kotlin/lotto/model/LottoStoreCashier.kt
user: login: Gyuil-Hwnag
body: 주석을 이렇게 추가하신 이유가 있을까요??
어떤 부분에는 있고 어떤 부분에는 없어서요~
created_at: 2025-02-28T02:22:56Z
author_association: MEMBER
diff_hunk: @@ -3,28 +3,34 @@ package lotto.model
 import lotto.contants.LottoRuleConstants
 
 class LottoTicket(
-    private val numbers: Set<LottoNumber> = generateLotto(),
+
+    private val lottoIssueType: LottoIssueType,
+    private val numbers: List<LottoNumber>,
path: src/main/kotlin/lotto/model/LottoTicket.kt
user: login: Gyuil-Hwnag
body: 급하셨나 보군요..!
띄어쓰기가ㅎㅎ😅
created_at: 2025-02-28T02:23:49Z
author_association: MEMBER
diff_hunk: @@ -3,28 +3,34 @@ package lotto.model
 import lotto.contants.LottoRuleConstants
 
 class LottoTicket(
-    private val numbers: Set<LottoNumber> = generateLotto(),
+
+    private val lottoIssueType: LottoIssueType,
+    private val numbers: List<LottoNumber>,
 ) {
+    private val lottoNumbers: Set<LottoNumber> = numbers.toSet()
+    
     init {
         require(numbers.size == LottoRuleConstants.LOTTO_PICK_COUNT.value)
         { ERROR_NUMBERS_COUNT }
     }
 
-    fun getSize() = numbers.size
+    fun getSize() = lottoNumbers.size
+
+    fun containsNumber(number: LottoNumber) : Boolean {
+        return lottoNumbers.contains(number)
+    }
+
+    fun getNumbers() : Set<LottoNumber> {
+        return lottoNumbers
+    }
 
-    fun getNumbers() = numbers
+    fun matchNumbersSize(lottoTicket: LottoTicket) = numbers.intersect(lottoTicket.getNumbers()).size
path: src/main/kotlin/lotto/model/LottoTicket.kt
user: login: Gyuil-Hwnag
body: 로또가 몇개가 맞는지에 대한 로직이 LottoTicket 내로 이동을 한 이유가 궁금합니다~🤔
created_at: 2025-02-28T02:26:56Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,27 @@
+package lotto.model
+
+// 수동이랑 자동 로또 계산해주는 매니저, 로또 티켓까지 주는 역할 담당
+class LottoTicketIssueManager (
+    customerWantBuyLottoTicketCount: Int,
+    customerWantToBuyManualLottoTicketCount: Int,
+    manualLottoNumbers: List<List<Int>>,
+) {
+    private val customerWantBuyLottoTicketCount = LottoTicketCount(customerWantBuyLottoTicketCount)
+    private val customerWantToBuyManualLottoTicketCount = LottoTicketCount(customerWantToBuyManualLottoTicketCount)
+    private val customerWantToBuyAutoLottoTicketCount = LottoTicketCount(customerWantBuyLottoTicketCount - customerWantToBuyManualLottoTicketCount)
+
+    // 발행가능한 개수보다 수동으로 로또 발행받고 싶은 개수가 크면 안된다.
+    init {
+        require(customerWantBuyLottoTicketCount >= customerWantToBuyManualLottoTicketCount) { "전체 발행 가능한 로또 개수보다 수동로또 발행 개수가 더 많습니다." }
+        require(customerWantToBuyManualLottoTicketCount == manualLottoNumbers.size) { LOTTO_COUNT_NOT_MATCH_LOTTO_NUMBERS }
+    }
path: src/main/kotlin/lotto/model/LottoTicketIssueManager.kt
user: login: Gyuil-Hwnag
body: 하드코딩된 내용들을 이전 단계처럼 상수화를 해보면 좋을 것 같습니다~
created_at: 2025-02-28T02:28:03Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,6 @@
+package lotto.model
+
+interface LottoTicketGenerator {
+    val type: LottoIssueType
+    fun generateLottoTicket() : LottoTicket
+}
path: src/main/kotlin/lotto/model/LottoTicketGenerator.kt
user: login: Gyuil-Hwnag
body: 인터페이스의 생성 정말 좋네요ㅎㅎ👍
created_at: 2025-02-28T02:30:44Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,27 @@
+package lotto.model
+
+// 수동이랑 자동 로또 계산해주는 매니저, 로또 티켓까지 주는 역할 담당
+class LottoTicketIssueManager (
+    customerWantBuyLottoTicketCount: Int,
+    customerWantToBuyManualLottoTicketCount: Int,
+    manualLottoNumbers: List<List<Int>>,
+) {
+    private val customerWantBuyLottoTicketCount = LottoTicketCount(customerWantBuyLottoTicketCount)
+    private val customerWantToBuyManualLottoTicketCount = LottoTicketCount(customerWantToBuyManualLottoTicketCount)
+    private val customerWantToBuyAutoLottoTicketCount = LottoTicketCount(customerWantBuyLottoTicketCount - customerWantToBuyManualLottoTicketCount)
+
+    // 발행가능한 개수보다 수동으로 로또 발행받고 싶은 개수가 크면 안된다.
+    init {
+        require(customerWantBuyLottoTicketCount >= customerWantToBuyManualLottoTicketCount) { "전체 발행 가능한 로또 개수보다 수동로또 발행 개수가 더 많습니다." }
+        require(customerWantToBuyManualLottoTicketCount == manualLottoNumbers.size) { LOTTO_COUNT_NOT_MATCH_LOTTO_NUMBERS }
+    }
+
+    // 로또를 발행해서 준다.
+    fun getLottoTickets(manualLottoNumbers: List<List<Int>>): List<LottoTicket> = LottoMachine().issueLottoTickets(customerWantToBuyAutoLottoTicketCount, manualLottoNumbers)
+
+    fun getCustomerWantToBuyManualLottoTicketCount(): Int = customerWantToBuyManualLottoTicketCount.toInt()
+
path: src/main/kotlin/lotto/model/LottoTicketIssueManager.kt
user: login: Gyuil-Hwnag
body: 오호 함수명이 어마무시하게 길군요..!🫣
무조건 짧아야 한다는 아니지만, 엄청 긴데 무슨 기능을 하는지 한눈에도 안들어오는 느낌이 살짝 드는군요~🤔
`LottoTicketIssueManager`전반적인 코멘트 입니다
created_at: 2025-02-28T02:32:09Z
author_association: MEMBER
diff_hunk: @@ -13,24 +13,53 @@ class UserInterface(
         val purchaseAmount = InputView.getUserInput()
         inputValidator.validateInteger(purchaseAmount)
         inputValidator.validateOverZero(purchaseAmount)
-        inputValidator.validateAmountUnits1000(purchaseAmount)
         return purchaseAmount.toInt()
     }
 
-    fun printLottoTickets(lottoTickets: List<LottoTicket>) {
-        OutputView.printMessage("${lottoTickets.size}개를 구매했습니다.")
+    fun printLottoCount(lottoCount: Int): Boolean {
+        OutputView.printMessage("\n발행할 수 있는 로또 수는 ${lottoCount}입니다. ${lottoCount}만큼 발행하겠습니까? Y / N 로 대답해주세요.")
+        val yesOrNo = InputView.getUserInput()
+        return yesOrNo == "Y"
+    }
+
+    fun printChange(change: Int) {
+        OutputView.printMessage("\n거스름 돈은 ${change}입니다.")
+    }
path: src/main/kotlin/lotto/view/UserInterface.kt
user: login: Gyuil-Hwnag
body: 하드 코딩된 문자열들을 상수화 해보는 것은 어떨까요??
추후 재사용에도 용이하고, 한곳에서 관리를 하여 가독성 측면에서도 좋을 수 있기는 합니다!
created_at: 2025-02-28T02:34:09Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,24 @@
+package lotto.model
+
+import lotto.contants.LottoRuleConstants
+
+// 돈 계산을 해주는 클래스
+class LottoStoreCashier(
path: src/main/kotlin/lotto/model/LottoStoreCashier.kt
user: login: wondroid-world
body: 개인적으로 이번 미션에서 목표가 객체지향적인 설계에 대해서 생각해보고 적용해보는 것입니다. 책에서 객체 하나당 한 역할을 하는 유기체라고 생각하라는 문구를 보았습니다. 그래서, 스스로 이 클래스는 무슨 역할을 한다고 정의하고 싶어서 주석을 추가했습니다. 😆 
created_at: 2025-03-03T04:09:49Z
author_association: NONE
diff_hunk: @@ -0,0 +1,16 @@
+package lotto.model
+
+import lotto.contants.LottoRuleConstants
+
+class AutoLottoTicketGenerator: LottoTicketGenerator {
+    override val type: LottoIssueType = LottoIssueType.AUTO
+
+    override fun generateLottoTicket(): LottoTicket {
+        val lottoNumbers = (LottoRuleConstants.MINIMUM_NUMBER.value..LottoRuleConstants.MAXIMUM_NUMBER.value)
+            .shuffled()
+            .take(LottoRuleConstants.LOTTO_PICK_COUNT.value)
+            .sorted()
+            .map(::LottoNumber)
+        return LottoTicket(LottoIssueType.AUTO, lottoNumbers)
+    }
path: src/main/kotlin/lotto/model/AutoLottoTicketGenerator.kt
user: login: wondroid-world
body: 현실에서 로또를 발행했을 때, 번호 옆에 자동, 수동이 표시가 되었습니다. 현실 로또 모양을 반영하고 싶어서 타입을 추가했습니다. 그래서 로또를 출력했을 때 아래와 같이 보이게 구현했습니다. 

<img width="178" alt="image" src="https://github.com/user-attachments/assets/ac92a486-ad06-4f2f-9f7e-ad09850abddf" />

created_at: 2025-03-03T04:19:05Z
author_association: NONE
diff_hunk: @@ -1,5 +1,25 @@
 package lotto.model
 
+// 로또 발행하는 머신
 class LottoMachine {
-    fun purchase(count: Int): List<LottoTicket> = List(count) { LottoTicket() }
+    // 로또 티켓을 발행한다.
+    fun issueLottoTickets(
+        customerWantToBuyAutoLottoTicketCount: LottoTicketCount,
+        manualLottoNumbers: List<List<Int>>,
+    ): List<LottoTicket> =
+        purchaseManualLottoTickets(manualLottoNumbers) + purchaseAutoLottoTickets(customerWantToBuyAutoLottoTicketCount)
path: src/main/kotlin/lotto/model/LottoMachine.kt
user: login: wondroid-world
body: 저는 로또 머신이 로또를 발행하는 주체라고 생각했습니다. '로또티켓을 발행한다'라는 의미에서 issue를 썼는데.. 음…. 의미가 잘 안 와닿았나 보네요. 이 부분은 조금 더 고민해보겠습니다!
created_at: 2025-03-03T04:31:13Z
author_association: NONE
diff_hunk: @@ -3,28 +3,34 @@ package lotto.model
 import lotto.contants.LottoRuleConstants
 
 class LottoTicket(
-    private val numbers: Set<LottoNumber> = generateLotto(),
+
+    private val lottoIssueType: LottoIssueType,
+    private val numbers: List<LottoNumber>,
 ) {
+    private val lottoNumbers: Set<LottoNumber> = numbers.toSet()
+    
     init {
         require(numbers.size == LottoRuleConstants.LOTTO_PICK_COUNT.value)
         { ERROR_NUMBERS_COUNT }
     }
 
-    fun getSize() = numbers.size
+    fun getSize() = lottoNumbers.size
+
+    fun containsNumber(number: LottoNumber) : Boolean {
+        return lottoNumbers.contains(number)
+    }
+
+    fun getNumbers() : Set<LottoNumber> {
+        return lottoNumbers
+    }
 
-    fun getNumbers() = numbers
+    fun matchNumbersSize(lottoTicket: LottoTicket) = numbers.intersect(lottoTicket.getNumbers()).size
path: src/main/kotlin/lotto/model/LottoTicket.kt
user: login: wondroid-world
body: 로또 번호가 몇 개 맞는지에 대한 로직이 LottoTicket으로 이동한 이유는 책임의 분리(SRP) 원칙을 따르기 위해서입니다.

WinningLotto 클래스는 "당첨된 로또 티켓과 다른 로또 티켓들을 비교"하는 책임을 가집니다. 이 클래스는 당첨 결과를 확인하는 역할에 집중해야 하며, 실제로 번호를 맞추는 로직은 LottoTicket 객체가 처리해야 할 일이라고 생각했습니다.

LottoTicket 클래스는 로또 번호를 담고 있는 객체로, 자신에게 맞는 번호가 몇 개인지 확인하는 역할을 맡는 것이 자연스럽다고 생각했습니다.

WinningLotto의 관심사는 "당첨된 로또 티켓과 비교하여 몇 등인지 확인하는 것"이지, 어떻게 번호가 맞는지를 구현하는 것은 LottoTicket의 책임이라고 생각햅니다. 각 객체는 자신의 책임에만 집중해야 하기 위해서, LottoTicket 내로 함수를 이동시켰습니다.
created_at: 2025-03-03T04:40:31Z
author_association: NONE
diff_hunk: @@ -0,0 +1,27 @@
+package lotto.model
+
+// 수동이랑 자동 로또 계산해주는 매니저, 로또 티켓까지 주는 역할 담당
+class LottoTicketIssueManager (
+    customerWantBuyLottoTicketCount: Int,
+    customerWantToBuyManualLottoTicketCount: Int,
+    manualLottoNumbers: List<List<Int>>,
+) {
+    private val customerWantBuyLottoTicketCount = LottoTicketCount(customerWantBuyLottoTicketCount)
+    private val customerWantToBuyManualLottoTicketCount = LottoTicketCount(customerWantToBuyManualLottoTicketCount)
+    private val customerWantToBuyAutoLottoTicketCount = LottoTicketCount(customerWantBuyLottoTicketCount - customerWantToBuyManualLottoTicketCount)
+
+    // 발행가능한 개수보다 수동으로 로또 발행받고 싶은 개수가 크면 안된다.
+    init {
+        require(customerWantBuyLottoTicketCount >= customerWantToBuyManualLottoTicketCount) { "전체 발행 가능한 로또 개수보다 수동로또 발행 개수가 더 많습니다." }
+        require(customerWantToBuyManualLottoTicketCount == manualLottoNumbers.size) { LOTTO_COUNT_NOT_MATCH_LOTTO_NUMBERS }
+    }
+
+    // 로또를 발행해서 준다.
+    fun getLottoTickets(manualLottoNumbers: List<List<Int>>): List<LottoTicket> = LottoMachine().issueLottoTickets(customerWantToBuyAutoLottoTicketCount, manualLottoNumbers)
+
+    fun getCustomerWantToBuyManualLottoTicketCount(): Int = customerWantToBuyManualLottoTicketCount.toInt()
+
path: src/main/kotlin/lotto/model/LottoTicketIssueManager.kt
user: login: wondroid-world
body: 하하하.... 저는 무조건 자세한 변수명이 좋은 건줄 알았습니다.
두루말처럼 간결한 변수명으로 변경해봤습니다!

eba76113b1f6053b673b0678b11631c2abc03f90
created_at: 2025-03-03T04:52:29Z
author_association: NONE
diff_hunk: @@ -13,24 +13,53 @@ class UserInterface(
         val purchaseAmount = InputView.getUserInput()
         inputValidator.validateInteger(purchaseAmount)
         inputValidator.validateOverZero(purchaseAmount)
-        inputValidator.validateAmountUnits1000(purchaseAmount)
         return purchaseAmount.toInt()
     }
 
-    fun printLottoTickets(lottoTickets: List<LottoTicket>) {
-        OutputView.printMessage("${lottoTickets.size}개를 구매했습니다.")
+    fun printLottoCount(lottoCount: Int): Boolean {
+        OutputView.printMessage("\n발행할 수 있는 로또 수는 ${lottoCount}입니다. ${lottoCount}만큼 발행하겠습니까? Y / N 로 대답해주세요.")
+        val yesOrNo = InputView.getUserInput()
+        return yesOrNo == "Y"
+    }
+
+    fun printChange(change: Int) {
+        OutputView.printMessage("\n거스름 돈은 ${change}입니다.")
+    }
path: src/main/kotlin/lotto/view/UserInterface.kt
user: login: wondroid-world
body: 두루! 두루말처럼 한번 해봤습니다. 이러니, 정말 문자열들이 한눈에 들어오네요! 나중에 관리하기 편하겠어요! 좋은 꿀팁 감사합니다!

d2499d4e7cb2b8b942b81d0b687a5b94eb573eb3
created_at: 2025-03-03T05:05:08Z
author_association: NONE
diff_hunk: @@ -0,0 +1,16 @@
+package lotto.model
+
+import lotto.contants.LottoRuleConstants
+
+class AutoLottoTicketGenerator: LottoTicketGenerator {
+    override val type: LottoIssueType = LottoIssueType.AUTO
+
+    override fun generateLottoTicket(): LottoTicket {
+        val lottoNumbers = (LottoRuleConstants.MINIMUM_NUMBER.value..LottoRuleConstants.MAXIMUM_NUMBER.value)
+            .shuffled()
+            .take(LottoRuleConstants.LOTTO_PICK_COUNT.value)
+            .sorted()
+            .map(::LottoNumber)
+        return LottoTicket(LottoIssueType.AUTO, lottoNumbers)
+    }
+}
path: src/main/kotlin/lotto/model/AutoLottoTicketGenerator.kt
user: login: wondroid-world
body: 어?! 저는 Android Studio를 쓰고 있지 않았는 데, 음... 제가 수동으로 merge하다가 괄호를 지운거 같습니다. 😭 
created_at: 2025-03-03T06:23:56Z
author_association: NONE
diff_hunk: @@ -1,39 +1,57 @@
 package lotto.controller
 
-import lotto.contants.LottoRuleConstants
-import lotto.model.LottoMachine
-import lotto.model.LottoResult
-import lotto.model.LottoTicket
-import lotto.model.WinningLotto
+import lotto.model.*
 import lotto.view.UserInterface
 
 class LottoController(
     private val userInterface: UserInterface = UserInterface(),
 ) {
-    fun run() {
-        val lottoTickets = generateLottoTickets()
-        val winningLotto = getWinningLotto()
-        val lottoResult = winningLotto.getResult(lottoTickets)
-        getResult(lottoResult)
+    fun run(): Result<Unit> {
+        return kotlin.runCatching {
+            val possibleToLottoTicketCount = meetLottoStoreCashier()
+            val lottoTickets = getLottoTickets(possibleToLottoTicketCount)
+            val winningLotto = getWinningLotto()
+            getResult(lottoTickets, winningLotto)
+        }.map {
+            Result.success(Unit)
+        }.getOrElse {
+            e ->  Result.failure(Exception("(로또 실행 중 오류 발생) : ${e.message}"))
+        }
path: src/main/kotlin/lotto/controller/LottoController.kt
user: login: wondroid-world
body: 사실, 이 구조가 나온것은 논리적 오류와 비논리적 오류에 대해서 잘 알지 못했습니다. 그래서, 비지니스 오류에서 나오는 것을 모두 출력하고 프로그램을 종료하고자 위와 같이 구현했습니다.

지금까지 정리한 생각
- 논리적 오류 : 비지니스 로직 상의 오류
- 비논리적 오류 : 네크워크 문제와 같이 외부적인 문제 오류
라고 생각합니다.

그래서 현재 구현한 로직은 사용자가 입력을 잘못하거나 비지니스 상 오류가 생겼을 때, 입력을 다시 받을 수 있도록 구현했습니다. 그래서 사용자가 저의 프로그램을 끝까지 사용할 수 있도록 했습니다. 
created_at: 2025-03-03T06:46:03Z
author_association: NONE
diff_hunk: @@ -0,0 +1,24 @@
+package lotto.model
+
+import lotto.contants.LottoRuleConstants
+
+// 돈 계산을 해주는 클래스
+class LottoStoreCashier(
path: src/main/kotlin/lotto/model/LottoStoreCashier.kt
user: login: Gyuil-Hwnag
body: 그래서 주석이 있는 객체가 존재하고 없는 객체가 존재했군요~
created_at: 2025-03-03T14:16:42Z
author_association: MEMBER
diff_hunk: @@ -3,28 +3,34 @@ package lotto.model
 import lotto.contants.LottoRuleConstants
 
 class LottoTicket(
-    private val numbers: Set<LottoNumber> = generateLotto(),
+
+    private val lottoIssueType: LottoIssueType,
+    private val numbers: List<LottoNumber>,
 ) {
+    private val lottoNumbers: Set<LottoNumber> = numbers.toSet()
+    
     init {
         require(numbers.size == LottoRuleConstants.LOTTO_PICK_COUNT.value)
         { ERROR_NUMBERS_COUNT }
     }
 
-    fun getSize() = numbers.size
+    fun getSize() = lottoNumbers.size
+
+    fun containsNumber(number: LottoNumber) : Boolean {
+        return lottoNumbers.contains(number)
+    }
+
+    fun getNumbers() : Set<LottoNumber> {
+        return lottoNumbers
+    }
 
-    fun getNumbers() = numbers
+    fun matchNumbersSize(lottoTicket: LottoTicket) = numbers.intersect(lottoTicket.getNumbers()).size
path: src/main/kotlin/lotto/model/LottoTicket.kt
user: login: Gyuil-Hwnag
body: WinningLotto는 로또의 당첨 번호만을 관리하고 LottoTicket 에서 내 번호가 당첨됐는지를 판별하도록 하셨군요~
모찌의 생각을 잘 알 수 있어서 좋네요!👍
created_at: 2025-03-03T14:19:31Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,17 @@
+package lotto.model
+
+import lotto.contants.LottoRuleConstants
+
+class AutoLottoTicketGenerator : LottoTicketGenerator {
+    override val type: LottoIssueType = LottoIssueType.AUTO
+
path: src/main/kotlin/lotto/model/AutoLottoTicketGenerator.kt
user: login: Gyuil-Hwnag
body: Generator는 생성자인데, model 에 있는것이 좋을까 싶긴 합니다!
다른 패키지로 옮긴다면 어떤 패키지로 옮겨볼 수 있을까요?🤔
created_at: 2025-03-03T14:20:30Z
author_association: MEMBER
diff_hunk: @@ -1,5 +1,24 @@
 package lotto.model
 
+// 로또 발행하는 머신
 class LottoMachine {
-    fun purchase(count: Int): List<LottoTicket> = List(count) { LottoTicket() }
+    // 로또 티켓을 발행한다.
+    fun issueLottoTickets(
+        customerWantToBuyAutoLottoTicketCount: LottoTicketCount,
+        manualLottoNumbers: List<List<Int>>,
+    ): List<LottoTicket> = purchaseManualLottoTickets(manualLottoNumbers) + purchaseAutoLottoTickets(customerWantToBuyAutoLottoTicketCount)
+
+    // 자동 로또를 발행
+    private fun purchaseAutoLottoTickets(customerWantToBuyAutoLottoTicketCount: LottoTicketCount): List<LottoTicket> {
+        val autoLottoGenerator = AutoLottoTicketGenerator()
+        val autoLottoTickets =
+            List(customerWantToBuyAutoLottoTicketCount.toInt()) { autoLottoGenerator.generateLottoTicket() }
+        return autoLottoTickets
+    }
+
+    // 수동 로또를 발행
+    private fun purchaseManualLottoTickets(manualLottoNumbers: List<List<Int>>): List<LottoTicket> {
+        val manualLottoTickets = manualLottoNumbers.map { ManualLottoTicketGenerator(it).generateLottoTicket() }
+        return manualLottoTickets
+    }
path: src/main/kotlin/lotto/model/LottoMachine.kt
user: login: Gyuil-Hwnag
body: 타입별 로또 티켓에 대한 생성자 좋습니다~👍
created_at: 2025-03-03T14:21:35Z
author_association: MEMBER
diff_hunk: @@ -1,5 +1,24 @@
 package lotto.model
 
+// 로또 발행하는 머신
 class LottoMachine {
-    fun purchase(count: Int): List<LottoTicket> = List(count) { LottoTicket() }
+    // 로또 티켓을 발행한다.
path: src/main/kotlin/lotto/model/LottoMachine.kt
user: login: Gyuil-Hwnag
body: class외에 object라는 키워드가 있는데, 이둘은 어떤 차이점이 있을까요?🤔
모찌의 생각을 남겨주세요!
각 두개의 키워드는 어떤 경우에 사용하면 좋을까요?
created_at: 2025-03-03T14:22:20Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,29 @@
+package lotto.model
+
+import lotto.contants.LottoRuleConstants
+
+// 돈 계산을 해주는 클래스
+class LottoStoreCashier(
+    private val customerMoney: Int,
+) {
+    private val possibleToLottoTicketCount = calculatePossibleToBuyLottoTicketCount()
+
+    // 로또의 가격은 천원이다. 로또를 구매할려면 천원이상 넣어야한다.
+    init {
+        require(customerMoney >= LottoRuleConstants.LOTTO_AMOUNT.value) { INSUFFICIENT_MONEY_FOR_LOTTO_PURCHASE }
+    }
+
+    // 고객이 준 돈에서 몇장을 살 수 있는 지 계산한다.
+    fun calculatePossibleToBuyLottoTicketCount(): Int = customerMoney / LottoRuleConstants.LOTTO_AMOUNT.value
+
+    // 고객이 몇장을 사고 싶다고 이야기를 하면 가능한지 계산한다.
+    fun isPossibleToBuy(customerWantBuyLottoTicketCount: Int): Boolean = possibleToLottoTicketCount >= customerWantBuyLottoTicketCount
+
+    // 고객에게 전달할 잔돈을 계산한다.
+    fun calculateChange(customerWantBuyLottoTicketCount: Int): Int =
+        customerMoney - (LottoRuleConstants.LOTTO_AMOUNT.value * possibleToLottoTicketCount)
+
+    companion object {
+        private const val INSUFFICIENT_MONEY_FOR_LOTTO_PURCHASE = "로또는 천원이상 넣어야지 구매 가능합니다."
+    }
path: src/main/kotlin/lotto/model/LottoStoreCashier.kt
user: login: Gyuil-Hwnag
body: [이전 코멘트](https://github.com/woowacourse/kotlin-lotto/pull/150#discussion_r1976848569)
해당 코멘트를 통해서 주석을 남겨주신 이유에 대해서는 너무 잘 이해를 했습니다!

다만, 한가지 고민을 해보면 좋을 것 같아서 코멘트를 남겨두었습니다~
복잡한 로직에 대해서는 주석을 남겨두면 어떤 로직인지에 대한 이해가 잘되도록 돕습니다.
그러나, 함수명 or 구현된 코드를 보고도 유추할 수 있는 모든 내용들에 주석이 존재한다면 오히려 구현을 이해하는데 혼동을 줄 수도 있기는 해서 추후 개발을 하면서 어떤 부분에 남겨야할지 모찌만의 기준을 잘 잡으셨으면 좋겠습니다💪
created_at: 2025-03-03T14:25:00Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,17 @@
+package lotto.model
+
+import lotto.contants.LottoRuleConstants
+
+class LottoTicketCount(
+    private val count: Int,
+) {
+    init {
+        require(count >= LottoRuleConstants.ZERO.value) { LOTTO_TICKET_COUNT_NOT_NEGATIVE }
+    }
+
+    fun toInt() = count
+
path: src/main/kotlin/lotto/model/LottoTicketCount.kt
user: login: Gyuil-Hwnag
body: toInt()로 표현을 하면, 어떤 것을 나타내는지에 대해서 불명확해질 수도 있겠다는 생각도 들긴 하네요!
다른 객체들에서 적용한 것처럼 해당 함수의 네이밍을 유의미하게 변경해볼 수 있겠다는 생각이 듭니다💪
created_at: 2025-03-03T14:27:17Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,29 @@
+package lotto.model
+
+// 수동이랑 자동 로또 계산해주는 매니저, 로또 티켓까지 주는 역할 담당
+class LottoTicketIssueManager(
+    totalCount: Int,
+    manualCount: Int,
+    manualLottoNumbers: List<List<Int>>,
+) {
+    private val manualLottoTicketCount = LottoTicketCount(manualCount)
+    private val autoLottoTicketCount =
+        LottoTicketCount(totalCount - manualCount)
+
+    // 발행가능한 개수보다 수동으로 로또 발행받고 싶은 개수가 크면 안된다.
+    init {
+        require(totalCount >= manualCount) { EXCEEDED_MANUAL_LOTTO_COUNT }
+        require(manualCount == manualLottoNumbers.size) { LOTTO_COUNT_NOT_MATCH_LOTTO_NUMBERS }
+    }
+
+    // 로또를 발행해서 준다.
+    fun getLottoTickets(manualLottoNumbers: List<List<Int>>): List<LottoTicket> =
+        LottoMachine().issueLottoTickets(autoLottoTicketCount, manualLottoNumbers)
+
+    fun getCustomerWantToBuyManualLottoTicketCount(): Int = autoLottoTicketCount.toInt()
+
path: src/main/kotlin/lotto/model/LottoTicketIssueManager.kt
user: login: Gyuil-Hwnag
body: 해당 함수의 네이밍을 아래와 같이 수정을 할수도 있을 것 같은데, 모찌의 생각에는 어떤 네이밍이 좋을 것 같나요🤔
물론 정답은 없습니다~
`getAutoLottoTicketCount()`
created_at: 2025-03-03T14:29:35Z
author_association: MEMBER
diff_hunk: @@ -1,39 +1,68 @@
 package lotto.controller
 
-import lotto.contants.LottoRuleConstants
-import lotto.model.LottoMachine
+import lotto.model.LottoIssueType
 import lotto.model.LottoResult
+import lotto.model.LottoStoreCashier
 import lotto.model.LottoTicket
+import lotto.model.LottoTicketIssueManager
 import lotto.model.WinningLotto
 import lotto.view.UserInterface
 
 class LottoController(
     private val userInterface: UserInterface = UserInterface(),
 ) {
path: src/main/kotlin/lotto/controller/LottoController.kt
user: login: Gyuil-Hwnag
body: 45 이상의 숫자를 입력하면 앱이 죽도록 되어 있군요~
이부분은 요구사항에 없긴 한데, 한번 고민해보면 좋을 것 같은 내용인 것 같아서 코멘트 남겨두었습니다!
재 입력을 받도록 해볼 수도 있겠다는 생각도 드는군요!💪

(수정이 이루어져야 하는 것은 아니고 어떻게 하면 해당 기능을 구현할 수 있을지 모찌의 의견을 남겨주셔도 좋을 것 같습니다)
created_at: 2025-03-03T14:33:03Z
author_association: MEMBER
diff_hunk: @@ -1,5 +1,24 @@
 package lotto.model
 
+// 로또 발행하는 머신
 class LottoMachine {
-    fun purchase(count: Int): List<LottoTicket> = List(count) { LottoTicket() }
path: src/main/kotlin/lotto/model/LottoMachine.kt
user: login: Gyuil-Hwnag
body: LottoMachine을 통해서 로또를 생성하는군요!
LottoMachine을 통해서 수동 로또 / 자동 로또가 재대로 생성되는지에 대한 테스트를 해볼 수 있겠군요💪
created_at: 2025-03-03T14:35:26Z
author_association: MEMBER
diff_hunk: @@ -1,5 +1,7 @@
 package lotto.view
 
 object OutputView {
-    fun printMessage(msg: Any) = println(msg)
+    fun printlnMessage(msg: Any) = println(msg)
+
+    fun printMessage(msg: Any) = print(msg)
 }
path: src/main/kotlin/lotto/view/OutputView.kt
user: login: Gyuil-Hwnag
body: OutputView와 InputView에서 너무 담당을 하는 것이 없다는 생각이 드네요..😅
`UserInterface`에서 입출력에 대해서 모든 기능을 담당하고 있으므로, 해당 InputView / OutputView의 내용을 UserInterface의 하나의 함수로 만들어 보는 것이 나을 수도 있겠다는 생각이 듭니다!
created_at: 2025-03-03T14:37:14Z
author_association: MEMBER
diff_hunk: @@ -8,69 +8,152 @@ import java.text.DecimalFormat
 class UserInterface(
     private val inputValidator: InputValidator = InputValidator(),
 ) {
path: src/main/kotlin/lotto/view/UserInterface.kt
user: login: Gyuil-Hwnag
body: Input / Output을 담당하는 UserInterface가 생겼으니, 로또의 InputOutput을 담당한다는 기능에 맞게 네이밍을 수정해보면 좋을 것 같다는 생각이 듭니다!💪
created_at: 2025-03-03T14:38:10Z
author_association: MEMBER
diff_hunk: @@ -7,23 +7,14 @@ import org.junit.jupiter.api.assertThrows
 class LottoTest {
     @Test
path: src/test/kotlin/lotto/model/LottoTest.kt
user: login: Gyuil-Hwnag
body: 테스트 코드에 대한 전반적인 코멘트 입니다!
https://developer.android.com/studio/test/test-in-android-studio#view_test_coverage
테스트코드를 어느 정도로 작성했는지 확인하는 방법으로 테스트 커버리지 라는 개념이 있습니다
IDEA에서 기능을 지원해 주고 있습니다

무조건 커버리지가 100을 달성해야 한다는 말 보다는 각 객체들이 담당하고 있는 기능들에 대해서는 어느정도의 테스트는 있어야 할 것 같다는 생각이 듭니다!💪
created_at: 2025-03-03T14:39:50Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,17 @@
+package lotto.model
+
+import lotto.contants.LottoRuleConstants
+
+class AutoLottoTicketGenerator : LottoTicketGenerator {
+    override val type: LottoIssueType = LottoIssueType.AUTO
+
path: src/main/kotlin/lotto/model/AutoLottoTicketGenerator.kt
user: login: wondroid-world
body: 아! 두루의 말은 generator는 비지니스의 핵심로직이니, 데이버 데이스를 관리하는 model보다 service라는 패키지를 만들어서 옮기면 좋다는 말일까요?!!
created_at: 2025-03-08T05:25:32Z
author_association: NONE
diff_hunk: @@ -0,0 +1,29 @@
+package lotto.model
+
+import lotto.contants.LottoRuleConstants
+
+// 돈 계산을 해주는 클래스
+class LottoStoreCashier(
+    private val customerMoney: Int,
+) {
+    private val possibleToLottoTicketCount = calculatePossibleToBuyLottoTicketCount()
+
+    // 로또의 가격은 천원이다. 로또를 구매할려면 천원이상 넣어야한다.
+    init {
+        require(customerMoney >= LottoRuleConstants.LOTTO_AMOUNT.value) { INSUFFICIENT_MONEY_FOR_LOTTO_PURCHASE }
+    }
+
+    // 고객이 준 돈에서 몇장을 살 수 있는 지 계산한다.
+    fun calculatePossibleToBuyLottoTicketCount(): Int = customerMoney / LottoRuleConstants.LOTTO_AMOUNT.value
+
+    // 고객이 몇장을 사고 싶다고 이야기를 하면 가능한지 계산한다.
+    fun isPossibleToBuy(customerWantBuyLottoTicketCount: Int): Boolean = possibleToLottoTicketCount >= customerWantBuyLottoTicketCount
+
+    // 고객에게 전달할 잔돈을 계산한다.
+    fun calculateChange(customerWantBuyLottoTicketCount: Int): Int =
+        customerMoney - (LottoRuleConstants.LOTTO_AMOUNT.value * possibleToLottoTicketCount)
+
+    companion object {
+        private const val INSUFFICIENT_MONEY_FOR_LOTTO_PURCHASE = "로또는 천원이상 넣어야지 구매 가능합니다."
+    }
path: src/main/kotlin/lotto/model/LottoStoreCashier.kt
user: login: wondroid-world
body: 아하! 알겠습니다! 감사합니다!
created_at: 2025-03-08T05:29:50Z
author_association: NONE
diff_hunk: @@ -0,0 +1,17 @@
+package lotto.model
+
+import lotto.contants.LottoRuleConstants
+
+class AutoLottoTicketGenerator : LottoTicketGenerator {
+    override val type: LottoIssueType = LottoIssueType.AUTO
+
path: src/main/kotlin/lotto/model/AutoLottoTicketGenerator.kt
user: login: Gyuil-Hwnag
body: 넵넵 맞습니다~
점점 서비스가 커질수록 많은 파일들이 생성될텐데, 이것을 잘 분리한다면 구조를 좀 더 쉽게 알 수 있었어서요
created_at: 2025-03-08T14:28:04Z
author_association: MEMBER
diff_hunk: @@ -5,59 +5,59 @@ import lotto.model.LottoTicket
 import lotto.model.Rank
 import java.text.DecimalFormat
 
-class UserInterface(
+class ViewFlow(
     private val inputValidator: InputValidator = InputValidator(),
 ) {
path: src/main/kotlin/lotto/view/ViewFlow.kt
user: login: Gyuil-Hwnag
body: ViewFlow로 변경이 되었군요..😅
사실 기대를 했던건 LottoView 등의 이름을 기대했었어요
created_at: 2025-03-08T14:29:45Z
author_association: MEMBER
diff_hunk: @@ -1,5 +0,0 @@
-package lotto.view
-
-object InputView {
-    fun getUserInput(): String = readln().trim()
-}
path: src/main/kotlin/lotto/view/InputView.kt
user: login: Gyuil-Hwnag
body: 사실 별다른 기능을 안하고 있는 InputView / OutputView의 삭제 좋습니다👍
created_at: 2025-03-08T14:30:10Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,15 @@
+package lotto.model
+
+import org.assertj.core.api.Assertions
+import org.junit.jupiter.api.Test
+
+class AutoLottoTicketGeneratorTest {
+    @Test
+    fun `로또 자동 발행하면, 자동 로또 티켓을 반환한다`() {
+        val actual = AutoLottoTicketGenerator().generateLottoTicket().lottoIssueType
+
+        val expected = LottoIssueType.AUTO
+
+        Assertions.assertThat(actual).isEqualTo(expected)
+    }
+}
path: src/test/kotlin/lotto/model/AutoLottoTicketGeneratorTest.kt
user: login: Gyuil-Hwnag
body: 다음 설정 옵션을 활성화하여 EOF 경고를 없애주세요~
File > Preferences > Editor > General > Ensure every saved file ends with a line break
created_at: 2025-03-08T14:32:02Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,40 @@
+package lotto.model
+
+import io.kotest.assertions.Expected
+import org.assertj.core.api.Assertions
+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 LottoStoreCashierTest {
+    @Test
+    fun `구입하는 돈이 1000원 보다 작으면 에러를 반환한다`() {
+        assertThrows<IllegalArgumentException> {
+            LottoStoreCashier(900)
+        }
+    }
+
+    @ParameterizedTest
+    @CsvSource(value = ["1000, 1"])
path: src/test/kotlin/lotto/model/LottoStoreCashierTest.kt
user: login: Gyuil-Hwnag
body: 파라미터 테스트의 활용 좋습니다~👍
created_at: 2025-03-08T14:32:40Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,50 @@
+package lotto.model
+
+import org.assertj.core.api.Assertions
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+
+class LottoTicketIssueManagerTest {
+    @Test
+    fun `수동 구매 개수와 수동 번호 입력 개수가 일치하지 않으면 에러를 발생한다`() {
+        val manualLottoCount = 0
+        val manualLottoNumbers = listOf(listOf(1, 2, 3, 4, 5, 6))
path: src/test/kotlin/lotto/model/LottoTicketIssueManagerTest.kt
user: login: Gyuil-Hwnag
body: 중복적으로 사용하는 테스트의 예시는 하나의 변수로 만들어서 관리를 한다면, 재사용을 통해서 직접 입력을 하는 고생을 줄여볼 수 있습니다~
created_at: 2025-03-08T14:33:35Z
author_association: MEMBER

PR comments:
user: login: wondroid-world
created_at: 2025-03-03T06:59:54Z
body: 두루,
한 객체는 한 acting만해야한다는 생각에 너무많은 객체를 생성한거 같아요. 그런데, 주석을 하나하나 달아보면 다 그만한 역할이 있고 또 이를 합치는 것은 한 객체가 너무 많은 역할을 하는 거 같아요. 나름의 기준을 잡아가는 과정이 꾀 어렵네요. 

두루 덕분에 네이밍에 대해서 많은 고민을 할 수 있어서 좋았습니다. 너무 자세한 네이밍을 하기 위해서 변수 이름이 길어지는 건 지양해야겠습니다. ㅎㅎㅎ
author_association: NONE
user: login: wondroid-world
created_at: 2025-03-08T08:08:29Z
body: 두루, 리뷰 요청이 늦었죠.... 죄송해여....

이번에 객체 지향에 대해서 혼자 고민해본다고 TDD부분에서 아주 많이 미흡했어요. 
두루 피드백 덕분에 뒤늦게라도 test를 구현할 수 있어서 다행이였습니다!
감사해요 두루!
author_association: NONE