diff --git a/docs/README.md b/docs/README.md
index e69de29bb..1e3336c32 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -0,0 +1,65 @@
+## 기능 목록(한 눈에 보기)
+
+```
+• 게임 시작 전
+ ◦ 경주할 자동차 이름을 입력받는다. (1) - InputView#getUserInput()
+ ▫︎ 자동차 이름은 쉼표로 구분한다. - Util#seperatorNameByComma()
+ ▫︎ 자동차 이름은 5자 이하여야 하며, 넘어갈 시 애러를 발생시킨다. (3) - Validator#validateLength()
+ ▫︎ 자동자 이름에 중복이 있다면 에러를 발생시킨다. (3) - Validator#validateUnique()
+ ▫︎ 자동차 이름에 null값이 들어오면 에러를 발생시킨다. (3) - Validator#validateNotNull()
+ ◦ 게임 횟수를 입력받는다. (2) - InputView#getUserInputCount()
+ ▫︎ 입력이 숫자가 아닐 시 이러를 발생시킨다. (3) - Validator#validateInteger()
+ ▫︎ 입력의 범위를 벗어날 시 에러를 발생시킨다. (3) - Validator#validateRange()
+
+• 게임 중
+ ◦ 0부터 9까지의 숫자를 랜덤하게 구한다. - MainController#getRandomNumber()
+ ▫︎ 구한 숫자가 4이상의 숫자인지 확인한다. - MainController#checkMovable()
+ ◦ 4이상의 숫자일 시 자동차를 전진한다. - Car#moveForward()
+ ▫︎ 자동차 전진은 '-'으로 표기한다. - OutputView#printDistance()
+ ▫︎ 단 첫번째 출력시에는 자동차 이름도 같이 표기한다. - OutputView#printCarName()
+ (ex. pobi : -)
+ ◦ 게임 횟수만큼 반복한다. - MainController#playOneCycle()
+
+• 게임 종료
+ ◦ 최종 우승자 리스트를 가져온다. - Cars#getWinners()
+ ◦ 최종 우승자를 출력한다. (4) - OutputView#printWinners()
+ ▫︎ 게임 우승자는 여러명이 나올 수도 있으며, 여러명일 경우에는 쉼표로 구분하여 출력한다.
+
+(1) 랜덤값 추출은 camp.nextstep.edu.missionutils.Randoms.pickNumberInRange()을 이용한다.
+(2) 사용자 입력은 camp.nextstep.edu.missionutils.Console.readLine()을 이용한다.
+(3) 예외처리는 IllegalArgumentException을 발생시켜 애플리케이션을 종료시킨다.
+(4) 게임 종료 시 System.exit()를 호출하지 않는다.
+```
+
+
+
+## 기능 목록 체크리스트
+
+- 게임 시작 전
+- [x] 게임 시작 문구를 출력한다.
+- [x] 자동차 경주 이름을 입력받는다.
+- [x] 자동차 이름들을 구분한다.
+- [x] 자동차 이름의 유효성을 검사한다.
+- [x] 게임 횟수를 입력받는다.
+- [x] 게임 횟수의 유효성을 검사한다.
+
+
+
+- 게임 시작
+- [x] 0부터 9까지의 숫자를 정한다.
+- [x] 만약 정한 숫자가 4보다 크면 한칸 움직인다.
+- [x] 처음에 입력 받는 횟수만큼 반복한다.
+
+
+
+- 게임 종료
+- [x] 최종 우승자를 출력한다.
+- [x] 여러 명일 경우에는 쉼표로 구분하여 출력한다.
+
+
+
+## 프로그래밍 요구 사항
+
+- [ ] indent(인덴트, 들여쓰기)는 depth를 3이 넘지 않도록 구현한다.
+- [ ] 함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라.
+- [ ] JUnit 5와 AssertJ를 이용하여 기능 목록이 정상적으로 작동하는지 테스트 코드를 작성하여 확인한다.
\ No newline at end of file
diff --git a/src/main/kotlin/racingcar/Application.kt b/src/main/kotlin/racingcar/Application.kt
index 0d8f3a79d..75f9c5607 100644
--- a/src/main/kotlin/racingcar/Application.kt
+++ b/src/main/kotlin/racingcar/Application.kt
@@ -1,5 +1,12 @@
package racingcar
+import racingcar.controller.MainController
+import racingcar.view.InputView
+import racingcar.view.OutputView
+
fun main() {
- // TODO: 프로그램 구현
+ val outputView = OutputView()
+ val inputView = InputView()
+ val controller = MainController(inputView, outputView)
+ controller.run()
}
diff --git a/src/main/kotlin/racingcar/controller/MainController.kt b/src/main/kotlin/racingcar/controller/MainController.kt
new file mode 100644
index 000000000..803ee9c6f
--- /dev/null
+++ b/src/main/kotlin/racingcar/controller/MainController.kt
@@ -0,0 +1,54 @@
+package racingcar.controller
+
+import racingcar.view.InputView
+import racingcar.view.OutputView
+import racingcar.model.Cars
+import racingcar.util.Util.getRandomNumber
+import racingcar.util.Util.isMovable
+
+class MainController(private val inputView: InputView, private val outputView: OutputView) {
+ private val cars = Cars()
+ private var gameCount = 0
+
+ fun run() {
+ gameInit()
+ gameStart()
+ gameEnd()
+ }
+
+ private fun gameStart() {
+ repeat(gameCount) {
+ playOneCycle()
+ }
+ }
+
+ private fun gameInit() {
+ inputCarNames()
+ inputGameCount()
+ }
+
+ private fun playOneCycle() {
+ cars.carNames.forEach { car ->
+ val isMovable = isMovable(getRandomNumber())
+ car.isMove(isMovable)
+ outputView.printCarDistance(car)
+ }
+ println()
+ }
+
+ private fun gameEnd() {
+ val winners = cars.getWinners()
+ outputView.printWinners(winners)
+ }
+
+ private fun inputCarNames() {
+ outputView.printInputCarNameMessage()
+ val carNames = inputView.getInputCarNames()
+ cars.addAllList(carNames)
+ }
+
+ private fun inputGameCount() {
+ outputView.printInputCountMessage()
+ gameCount = inputView.getInputCount()
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/racingcar/model/Car.kt b/src/main/kotlin/racingcar/model/Car.kt
new file mode 100644
index 000000000..fefc622cd
--- /dev/null
+++ b/src/main/kotlin/racingcar/model/Car.kt
@@ -0,0 +1,17 @@
+package racingcar.model
+
+class Car(private val _name: String) {
+
+ val name: String
+ get() = _name
+
+ private var _distance = 0
+ val distance: Int
+ get() = _distance
+
+ fun isMove(movable: Boolean) {
+ if (movable) _distance++
+ }
+
+ fun isWinner(maxValue: Int): Boolean = _distance == maxValue
+}
\ No newline at end of file
diff --git a/src/main/kotlin/racingcar/model/Cars.kt b/src/main/kotlin/racingcar/model/Cars.kt
new file mode 100644
index 000000000..24df1eee7
--- /dev/null
+++ b/src/main/kotlin/racingcar/model/Cars.kt
@@ -0,0 +1,16 @@
+package racingcar.model
+
+class Cars {
+ private var _carNames = mutableListOf()
+ val carNames: List
+ get() = _carNames
+
+ fun addAllList(carNames: List) = _carNames.addAll(carNames.map { Car(it) })
+
+ fun getMaxDistance(): Int = _carNames.maxOf { it.distance }
+
+ fun getWinners(): List {
+ val maxDistance = getMaxDistance()
+ return _carNames.filter { it.isWinner(maxDistance) }.map { it.name }
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/racingcar/util/Constants.kt b/src/main/kotlin/racingcar/util/Constants.kt
new file mode 100644
index 000000000..d9914ce6f
--- /dev/null
+++ b/src/main/kotlin/racingcar/util/Constants.kt
@@ -0,0 +1,9 @@
+package racingcar.util
+
+object Constants {
+ const val MAX_NAME_LENGTH = 5
+ const val START_NUMBER = 0
+ const val END_NUMBER = 9
+ const val MIN_COUNT_NUMBER = 1
+ const val MOVABLE_NUMBER = 4
+}
\ No newline at end of file
diff --git a/src/main/kotlin/racingcar/util/ExceptionMessage.kt b/src/main/kotlin/racingcar/util/ExceptionMessage.kt
new file mode 100644
index 000000000..8f6114f6e
--- /dev/null
+++ b/src/main/kotlin/racingcar/util/ExceptionMessage.kt
@@ -0,0 +1,11 @@
+package racingcar.util
+
+enum class ExceptionMessage(private val message: String) {
+ INVALID_LENGTH("자동차의 이름이 5보다 깁니다."),
+ INVALID_UNIQUE_NAME("중복된 자동차의 이름이 존재합니다."),
+ INVALID_CAR_NAME("자동차 이름에 널값이 존재합니다."),
+ INVALID_INTEGER("사용자의 입력이 정수가 아닙니다."),
+ INVALID_RANGE("사용자의 입력이 유효한 범위가 아닙니다.");
+
+ fun getMessage(): String = message
+}
\ No newline at end of file
diff --git a/src/main/kotlin/racingcar/util/Util.kt b/src/main/kotlin/racingcar/util/Util.kt
new file mode 100644
index 000000000..d056893d3
--- /dev/null
+++ b/src/main/kotlin/racingcar/util/Util.kt
@@ -0,0 +1,11 @@
+package racingcar.util
+
+import camp.nextstep.edu.missionutils.Randoms
+
+object Util {
+ fun separateNameByComma(carNames: String): List = carNames.split(",")
+
+ fun getRandomNumber(): Int = Randoms.pickNumberInRange(Constants.START_NUMBER, Constants.END_NUMBER)
+
+ fun isMovable(number: Int): Boolean = number >= Constants.MOVABLE_NUMBER
+}
\ No newline at end of file
diff --git a/src/main/kotlin/racingcar/util/Validator.kt b/src/main/kotlin/racingcar/util/Validator.kt
new file mode 100644
index 000000000..be6cff345
--- /dev/null
+++ b/src/main/kotlin/racingcar/util/Validator.kt
@@ -0,0 +1,33 @@
+package racingcar.util
+
+import racingcar.util.Constants.MAX_NAME_LENGTH
+import racingcar.util.Constants.MIN_COUNT_NUMBER
+import java.lang.IllegalArgumentException
+
+object Validator {
+ fun validateLength(numberList: List) {
+ numberList.forEach {
+ if (it.length > MAX_NAME_LENGTH) throw IllegalArgumentException(ExceptionMessage.INVALID_LENGTH.getMessage())
+ }
+ }
+
+ fun validateUnique(numberList: List) {
+ val validation = numberList.toSet()
+ if (validation.size != numberList.size) throw IllegalArgumentException(ExceptionMessage.INVALID_UNIQUE_NAME.getMessage())
+ }
+
+ fun validateNotNull(numberList: List) {
+ numberList.forEach {
+ if (it.trim().isEmpty()) throw IllegalArgumentException(ExceptionMessage.INVALID_CAR_NAME.getMessage())
+ }
+ }
+
+ fun validateInteger(number: String) {
+ number.toIntOrNull() ?: throw IllegalArgumentException(ExceptionMessage.INVALID_INTEGER.getMessage())
+ }
+
+ fun validateRange(number: String) {
+ val validation = number.toInt()
+ if (validation !in MIN_COUNT_NUMBER..Int.MAX_VALUE) throw IllegalArgumentException(ExceptionMessage.INVALID_RANGE.getMessage())
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/racingcar/view/InputView.kt b/src/main/kotlin/racingcar/view/InputView.kt
new file mode 100644
index 000000000..d9b72e3b8
--- /dev/null
+++ b/src/main/kotlin/racingcar/view/InputView.kt
@@ -0,0 +1,28 @@
+package racingcar.view
+
+import camp.nextstep.edu.missionutils.Console
+import racingcar.util.Util.separateNameByComma
+import racingcar.util.Validator.validateInteger
+import racingcar.util.Validator.validateLength
+import racingcar.util.Validator.validateNotNull
+import racingcar.util.Validator.validateRange
+import racingcar.util.Validator.validateUnique
+
+class InputView {
+ private fun getUserInput(): String = Console.readLine()
+
+ fun getInputCarNames(): List {
+ val input = separateNameByComma(getUserInput())
+ validateLength(input)
+ validateUnique(input)
+ validateNotNull(input)
+ return input
+ }
+
+ fun getInputCount(): Int {
+ val input = getUserInput()
+ validateInteger(input)
+ validateRange(input)
+ return input.toInt()
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/racingcar/view/OutputView.kt b/src/main/kotlin/racingcar/view/OutputView.kt
new file mode 100644
index 000000000..2b7ba7aa7
--- /dev/null
+++ b/src/main/kotlin/racingcar/view/OutputView.kt
@@ -0,0 +1,22 @@
+package racingcar.view
+
+import racingcar.model.Car
+
+class OutputView {
+ fun printInputCarNameMessage() {
+ println("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)")
+ }
+
+ fun printInputCountMessage() {
+ println("시도할 횟수는 몇 회인가요?")
+ }
+
+ fun printCarDistance(car: Car) {
+ println("${car.name} : ${"-".repeat(car.distance)}")
+ }
+
+ fun printWinners(winners: List) {
+ val output = winners.joinToString(", ")
+ println("최종 우승자 : $output")
+ }
+}
\ No newline at end of file
diff --git a/src/test/kotlin/racingcar/ApplicationTest.kt b/src/test/kotlin/racingcar/ApplicationTest.kt
index 2cb36835c..bdc896bdf 100644
--- a/src/test/kotlin/racingcar/ApplicationTest.kt
+++ b/src/test/kotlin/racingcar/ApplicationTest.kt
@@ -3,11 +3,27 @@ package racingcar
import camp.nextstep.edu.missionutils.test.Assertions.assertRandomNumberInRangeTest
import camp.nextstep.edu.missionutils.test.Assertions.assertSimpleTest
import camp.nextstep.edu.missionutils.test.NsTest
+import racingcar.model.Car
+import racingcar.model.Cars
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
+import racingcar.controller.MainController
+import racingcar.util.Util.isMovable
+import racingcar.util.Util.separateNameByComma
+import racingcar.util.Validator.validateInteger
+import racingcar.util.Validator.validateLength
+import racingcar.util.Validator.validateNotNull
+import racingcar.util.Validator.validateRange
+import racingcar.util.Validator.validateUnique
+import racingcar.view.InputView
+import racingcar.view.OutputView
class ApplicationTest : NsTest() {
+ private val inputView = InputView()
+ private val outputView = OutputView()
+ private val controller = MainController(inputView, outputView)
+
@Test
fun `전진 정지`() {
assertRandomNumberInRangeTest(
@@ -26,6 +42,123 @@ class ApplicationTest : NsTest() {
}
}
+ @Test
+ fun `컴마로 구분하여 리스트 반환 검증`() {
+ val input = "lh99j,pobi"
+ val validation = separateNameByComma(input)
+ val result = listOf("lh99j", "pobi")
+ assertThat(validation).isEqualTo(result)
+ }
+
+ @Test
+ fun `자동차 이름 길이 검증`() {
+ assertSimpleTest {
+ assertThrows { validateLength(listOf("pobi", "javaji")) }
+ assertThrows { validateLength(listOf("abcdef, abcdefg")) }
+
+ }
+ }
+
+ @Test
+ fun `자동차 이름 중복 검증`() {
+ assertSimpleTest {
+ assertThrows { validateUnique(listOf("pobi", "javaji", "pobi")) }
+ assertThrows { validateUnique(listOf("a", "b", "c", "a")) }
+
+ }
+ }
+
+ @Test
+ fun `자동차 이름 널값 검증`() {
+ assertSimpleTest {
+ assertThrows { validateNotNull(listOf("pobi", "javaji", " ")) }
+ assertThrows { validateNotNull(listOf("a", "b", "c", "")) }
+ assertThrows { validateNotNull(listOf("a", "b", " ")) }
+ }
+ }
+
+ @Test
+ fun `사용자 입력 정수 검증`() {
+ assertSimpleTest {
+ assertThrows { validateInteger("a") }
+ assertThrows { validateInteger("1a") }
+ assertThrows { validateInteger("4_@") }
+ }
+ }
+
+ @Test
+ fun `사용자 입력 유효 범위 검증`() {
+ assertSimpleTest {
+ assertThrows { validateRange((Int.MAX_VALUE.toLong() + 1).toString()) }
+ assertThrows { validateRange("-1") }
+ assertThrows { validateRange("0") }
+ }
+ }
+
+ @Test
+ fun `자동차가 움직였는지 검증`() {
+ val car = Car("lh99j")
+ car.isMove(true)
+ val validation = car.distance
+ val result = 1
+ assertThat(validation).isEqualTo(result)
+ }
+
+ @Test
+ fun `움직일 수 있는지 반환 기능 검증`() {
+ val falseInput = 3
+ val falseValidation = isMovable(falseInput)
+ val falseResult = false
+ assertThat(falseValidation).isEqualTo(falseResult)
+
+ val trueInput = 4
+ val trueValidation = isMovable(trueInput)
+ val trueResult = true
+ assertThat(trueValidation).isEqualTo(trueResult)
+ }
+
+ @Test
+ fun `최대 거리 반환 기능 검증`() {
+ val input = listOf("pobi", "lh99j", "anjji", "amin")
+ val cars = Cars()
+ cars.addAllList(input)
+ cars.carNames[1].isMove(true)
+ cars.carNames[1].isMove(true)
+ cars.carNames[1].isMove(true)
+ cars.carNames[1].isMove(true)
+ val validation = cars.getMaxDistance()
+ val result = 4
+ assertThat(validation).isEqualTo(result)
+ }
+
+ @Test
+ fun `우승자 확인 기능 검증`() {
+ val input = listOf(Car("pobi"), Car("lh99j"), Car("abc"))
+ input[1].isMove(true)
+ input[1].isMove(true)
+
+ val falseValidation = input[0].isWinner(2)
+ val falseResult = false
+ assertThat(falseValidation).isEqualTo(falseResult)
+
+ val trueValidation = input[1].isWinner(2)
+ val trueResult = true
+ assertThat(trueValidation).isEqualTo(trueResult)
+ }
+
+ @Test
+ fun `우승자 반환 기능 검증`() {
+ val input = listOf("pobi", "lh99j", "abcd")
+ val cars = Cars()
+ cars.addAllList(input)
+ cars.carNames[0].isMove(true)
+ cars.carNames[1].isMove(true)
+
+ val validation = cars.getWinners()
+ val result = listOf("pobi", "lh99j")
+ assertThat(validation).isEqualTo(result)
+ }
+
public override fun runMain() {
main()
}