diff --git a/README.md b/README.md
new file mode 100644
index 00000000..a9823886
--- /dev/null
+++ b/README.md
@@ -0,0 +1,76 @@
+## 움직이는 자동차 [O]
+### 기능 요구사항
+- 자동차는 이름을 가지고 있다.
+- 자동차는 움직일 수 있다.
+- 0에서 9 사이에서 random 값을 구한 후 random 값이 4 이상일 경우 전진하고, 3 이하의 값이면 멈춘다.
+### 새로운 프로그래밍 요구사항
+- 자동차가 움직이는 기능이 의도대로 동작하는지 테스트한다.
+- 자바 코드 컨벤션을 지키면서 프로그래밍한다.
+- 기본적으로 Java Style Guide을 원칙으로 한다.
+- indent(인덴트, 들여쓰기) depth를 2를 넘지 않도록 구현한다. 1까지만 허용한다.
+ - 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.
+ - 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면 된다.
+- 3항 연산자를 쓰지 않는다.
+- else 예약어를 쓰지 않는다.
+ - else 예약어를 쓰지 말라고 하니 switch/case로 구현하는 경우가 있는데 switch/case도 허용하지 않는다.
+ - 힌트: if문에서 값을 반환하는 방식으로 구현하면 else 예약어를 사용하지 않아도 된다.
+- 함수(또는 메소드)의 길이가 15라인을 넘어가지 않도록 구현한다.
+ - 함수(또는 메소드)가 한 가지 일만 잘 하도록 구현한다.
+### 기존 프로그래밍 요구사항
+- 메인 메서드는 만들지 않는다.
+
+## 우승 자동차 구하기 [O]
+### 기능 요구사항
+- n대의 자동차가 참여할 수 있다.
+- 주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다.
+ - 0에서 9 사이에서 random 값을 구한 후 random 값이 4 이상일 경우 전진하고, 3 이하의 값이면 멈춘다.
+- 자동차 경주 게임을 완료한 후 누가 우승했는지를 구할 수 있다. 우승자는 한 명 이상일 수 있다.
+### 새로운 프로그래밍 요구사항
+- 우승자를 구하는 기능이 의도대로 동작하는지 테스트한다.
+
+## 게임 실행 [O]
+### 기능 요구사항
+- 주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다.
+- 각 자동차에 이름을 부여할 수 있다. 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다.
+- 자동차 이름은 쉼표(,)를 기준으로 구분하며 이름은 5자 이하만 가능하다.
+- 사용자는 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다.
+- 전진하는 조건은 0에서 9 사이에서 random 값을 구한 후 random 값이 4 이상일 경우 전진하고, 3 이하의 값이면 멈춘다.
+- 자동차 경주 게임을 완료한 후 누가 우승했는지를 알려준다. 우승자는 한 명 이상일 수 있다.
+
**실행 결과**
+- 위 요구사항에 따라 3대의 자동차가 5번 움직였을 경우 프로그램을 실행한 결과는 다음과 같다.
+```
+경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분).
+neo,brie,brown
+시도할 회수는 몇회인가요?
+5
+
+실행 결과
+neo : -
+brie : -
+brown : -
+
+neo : --
+brie : -
+brown : --
+
+neo : ---
+brie : --
+brown : ---
+
+neo : ----
+brie : ---
+brown : ----
+
+neo : -----
+brie : ----
+brown : -----
+
+neo : -----
+brie : ----
+brown : -----
+
+neo, brown가 최종 우승했습니다.
+```
+
+### 새로운 프로그래밍 요구사항
+- 메인 메서드를 추가하여 실행 가능한 애플리케이션으로 만든다.
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 239f9e78..8d5f4636 100644
--- a/build.gradle
+++ b/build.gradle
@@ -14,6 +14,9 @@ dependencies {
testImplementation platform('org.assertj:assertj-bom:3.25.1')
testImplementation('org.junit.jupiter:junit-jupiter')
testImplementation('org.assertj:assertj-core')
+ testImplementation("org.mockito:mockito-core:5.14.1")
+ compileOnly("org.projectlombok:lombok:1.18.34")
+ annotationProcessor('org.projectlombok:lombok:1.18.34')
}
test {
diff --git a/src/main/java/.gitkeep b/src/main/java/.gitkeep
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/main/java/RacingCarGame.java b/src/main/java/RacingCarGame.java
new file mode 100644
index 00000000..453a0b2d
--- /dev/null
+++ b/src/main/java/RacingCarGame.java
@@ -0,0 +1,19 @@
+import controller.RacingCarController;
+import dao.RacingCarDao;
+import service.RacingCarService;
+import service.RacingCarServiceImpl;
+import util.NumberGenerateUtil;
+import view.RacingCarView;
+
+public class RacingCarGame {
+ public static void main(String[] args) {
+ RacingCarView racingCarView = new RacingCarView();
+ RacingCarDao racingCarDao = new RacingCarDao();
+ NumberGenerateUtil numberGenerateUtil = new NumberGenerateUtil();
+ RacingCarService racingCarService = new RacingCarServiceImpl(racingCarDao, numberGenerateUtil);
+ RacingCarController game = new RacingCarController(racingCarView, racingCarService);
+
+ game.set();
+ game.run();
+ }
+}
diff --git a/src/main/java/controller/RacingCarController.java b/src/main/java/controller/RacingCarController.java
new file mode 100644
index 00000000..4b03f50b
--- /dev/null
+++ b/src/main/java/controller/RacingCarController.java
@@ -0,0 +1,39 @@
+package controller;
+
+import converter.RacingCarConverter;
+import lombok.RequiredArgsConstructor;
+import service.RacingCarService;
+import view.RacingCarView;
+
+@RequiredArgsConstructor
+public class RacingCarController {
+
+ final RacingCarView racingCarView;
+ final RacingCarService racingCarService;
+
+ int round;
+ boolean isGameInit = false;
+
+ public void set() {
+ racingCarService.createRacingCar(racingCarView.inputRacingCarName().stream().map(
+ RacingCarConverter::toRacingCarCreateDto).toList());
+ round = racingCarView.inputRound();
+
+ isGameInit = true;
+ }
+
+ public void run() {
+ if (!isGameInit)
+ throw new IllegalStateException("게임이 초기화되지 않았습니다.");
+
+ racingCarView.printResultTitle();
+ for(int currentRound = 0; currentRound < round; ++currentRound){
+ racingCarService.move();
+ racingCarView.printResult(racingCarService.getResults());
+ }
+
+ racingCarView.printWinner(racingCarService.getResults());
+
+ isGameInit = false;
+ }
+}
diff --git a/src/main/java/converter/RacingCarConverter.java b/src/main/java/converter/RacingCarConverter.java
new file mode 100644
index 00000000..8486fbb5
--- /dev/null
+++ b/src/main/java/converter/RacingCarConverter.java
@@ -0,0 +1,27 @@
+package converter;
+
+import domain.RacingCar;
+import dto.RacingCarCreateDto;
+import dto.RacingCarResultDto;
+import java.util.stream.Collectors;
+
+public class RacingCarConverter {
+ public static RacingCarCreateDto toRacingCarCreateDto(String name) {
+ return RacingCarCreateDto.builder().name(name).build();
+ }
+
+ public static RacingCarResultDto toRacingCarResultDto(RacingCar racingCar) {
+ return RacingCarResultDto.builder().
+ name(racingCar.getName())
+ .resultString(racingCar.getResults().stream().map(RacingCarConverter::resultToString).collect(
+ Collectors.joining()))
+ .distance((int) racingCar.getResults().stream().filter(v -> v).count()).build();
+ }
+
+ public static String resultToString(Boolean result) {
+ if (result)
+ return "-";
+
+ return "";
+ }
+}
diff --git a/src/main/java/dao/RacingCarDao.java b/src/main/java/dao/RacingCarDao.java
new file mode 100644
index 00000000..a6075410
--- /dev/null
+++ b/src/main/java/dao/RacingCarDao.java
@@ -0,0 +1,20 @@
+package dao;
+
+import domain.RacingCar;
+import dto.RacingCarCreateDto;
+import java.util.ArrayList;
+import java.util.List;
+
+public class RacingCarDao {
+ private final List racingCars = new ArrayList<>();
+
+ public RacingCar insert(RacingCarCreateDto racingCarCreateDto) {
+ RacingCar racingCar = new RacingCar(racingCarCreateDto.getName());
+ racingCars.add(racingCar);
+ return racingCar;
+ }
+
+ public List select() {
+ return racingCars;
+ }
+}
diff --git a/src/main/java/domain/RacingCar.java b/src/main/java/domain/RacingCar.java
new file mode 100644
index 00000000..813e634f
--- /dev/null
+++ b/src/main/java/domain/RacingCar.java
@@ -0,0 +1,15 @@
+package domain;
+
+import java.util.ArrayList;
+import java.util.List;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
+
+@Getter
+@Setter
+@RequiredArgsConstructor
+public class RacingCar {
+ private final String name;
+ private final List results = new ArrayList<>();
+}
diff --git a/src/main/java/dto/RacingCarCreateDto.java b/src/main/java/dto/RacingCarCreateDto.java
new file mode 100644
index 00000000..536c6fcb
--- /dev/null
+++ b/src/main/java/dto/RacingCarCreateDto.java
@@ -0,0 +1,14 @@
+package dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Builder
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+public class RacingCarCreateDto {
+ private String name;
+}
diff --git a/src/main/java/dto/RacingCarResultDto.java b/src/main/java/dto/RacingCarResultDto.java
new file mode 100644
index 00000000..29d0d9eb
--- /dev/null
+++ b/src/main/java/dto/RacingCarResultDto.java
@@ -0,0 +1,16 @@
+package dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Builder
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+public class RacingCarResultDto {
+ private String name;
+ private String resultString;
+ private int distance;
+}
diff --git a/src/main/java/service/RacingCarService.java b/src/main/java/service/RacingCarService.java
new file mode 100644
index 00000000..31329d38
--- /dev/null
+++ b/src/main/java/service/RacingCarService.java
@@ -0,0 +1,13 @@
+package service;
+
+import dto.RacingCarCreateDto;
+import dto.RacingCarResultDto;
+import java.util.List;
+
+public interface RacingCarService {
+ void createRacingCar(List racingCarCreateDtoList);
+
+ void move();
+
+ List getResults();
+}
diff --git a/src/main/java/service/RacingCarServiceImpl.java b/src/main/java/service/RacingCarServiceImpl.java
new file mode 100644
index 00000000..7cfb31e0
--- /dev/null
+++ b/src/main/java/service/RacingCarServiceImpl.java
@@ -0,0 +1,38 @@
+package service;
+
+import converter.RacingCarConverter;
+import dao.RacingCarDao;
+import domain.RacingCar;
+import dto.RacingCarCreateDto;
+import dto.RacingCarResultDto;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import util.NumberGenerateUtil;
+
+@RequiredArgsConstructor
+public class RacingCarServiceImpl implements RacingCarService {
+ final RacingCarDao racingCarDao;
+ final NumberGenerateUtil numberGenerateUtil;
+
+ @Override
+ public void createRacingCar(final List racingCarCreateDtoList) {
+ List racingCars = racingCarCreateDtoList.stream().map(racingCarDao::insert).toList();
+ }
+
+ @Override
+ public void move() {
+ for (RacingCar racingCar : racingCarDao.select()){
+ int power = numberGenerateUtil.generateRandomNumber();
+
+ if (0 <= power && power <= 9)
+ racingCar.getResults().add(power >= 4);
+ else
+ throw new NumberFormatException("0~9 사이의 Power 값을 필요로 합니다.");
+ }
+ }
+
+ @Override
+ public List getResults() {
+ return racingCarDao.select().stream().map(RacingCarConverter::toRacingCarResultDto).toList();
+ }
+}
diff --git a/src/main/java/util/NumberGenerateUtil.java b/src/main/java/util/NumberGenerateUtil.java
new file mode 100644
index 00000000..2acba90b
--- /dev/null
+++ b/src/main/java/util/NumberGenerateUtil.java
@@ -0,0 +1,11 @@
+package util;
+
+import java.security.SecureRandom;
+
+public class NumberGenerateUtil {
+
+ public int generateRandomNumber() {
+ SecureRandom secureRandom = new SecureRandom();
+ return secureRandom.nextInt(10);
+ }
+}
diff --git a/src/main/java/view/RacingCarView.java b/src/main/java/view/RacingCarView.java
new file mode 100644
index 00000000..24875ea7
--- /dev/null
+++ b/src/main/java/view/RacingCarView.java
@@ -0,0 +1,78 @@
+package view;
+
+import dto.RacingCarResultDto;
+import java.lang.module.ResolutionException;
+import java.util.InputMismatchException;
+import java.util.List;
+import java.util.Scanner;
+import java.util.regex.Pattern;
+
+
+public class RacingCarView {
+ public static class RacingCarInputException extends RuntimeException {
+
+ public RacingCarInputException() {
+ super("유효하지 않은 입력입니다.");
+ }
+
+ public RacingCarInputException(final String s) {
+ super(s);
+ }
+ }
+
+ public List inputRacingCarName() {
+ Scanner scanner = new Scanner(System.in);
+
+ System.out.println("경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분).");
+ String input = scanner.nextLine().replaceAll(" ", "");
+
+ if (Pattern.compile("[!@#$%^&*().?\"{}|<>]").matcher(input).find())
+ throw new RacingCarInputException("이름에 특수기호를 입력할 수 없습니다.");
+
+ List names = List.of(input.split(","));
+
+ if (names.size() <= 1)
+ throw new RacingCarInputException("최소한 두 명 이상의 플레이어가 필요합니다.");
+
+
+ return names;
+ }
+
+ public int inputRound() {
+ Scanner scanner = new Scanner(System.in);
+
+ System.out.println("시도할 회수는 몇회인가요?");
+
+ try {
+ int round = scanner.nextInt();
+
+ if (round <= 0)
+ throw new NumberFormatException("횟수는 1회 이상이여야 합니다.");
+
+ return round;
+ } catch (InputMismatchException e) {
+ throw new RacingCarInputException();
+ } catch (NumberFormatException e) {
+ throw new RacingCarInputException(e.getMessage());
+ }
+ }
+
+ public void printResultTitle() {
+ System.out.println("\n실행 결과");
+ }
+
+ public void printResult(List racingCarResultDtoList) {
+ for (RacingCarResultDto racingCarResultDto : racingCarResultDtoList) {
+ System.out.printf("%s : %s%n", racingCarResultDto.getName(), racingCarResultDto.getResultString());
+ }
+ System.out.println();
+ }
+
+ public void printWinner(List racingCarResultDtoList) {
+ int maxDistance = racingCarResultDtoList.stream().mapToInt(RacingCarResultDto::getDistance).max().orElseThrow();
+
+ List winners = racingCarResultDtoList.stream().filter(v -> v.getDistance() == maxDistance).map(RacingCarResultDto::getName).toList();
+
+ System.out.println(String.join(", ", winners) + "가 최종 우승했습니다.");
+ }
+}
diff --git a/src/test/java/.gitkeep b/src/test/java/.gitkeep
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/test/java/RacingCarConverterTest.java b/src/test/java/RacingCarConverterTest.java
new file mode 100644
index 00000000..16537fd4
--- /dev/null
+++ b/src/test/java/RacingCarConverterTest.java
@@ -0,0 +1,38 @@
+import static org.assertj.core.api.Assertions.assertThat;
+
+import converter.RacingCarConverter;
+import domain.RacingCar;
+import dto.RacingCarCreateDto;
+import dto.RacingCarResultDto;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+public class RacingCarConverterTest {
+
+ @Test
+ public void testToRacingCarCreateDto() {
+ RacingCarCreateDto expected = new RacingCarCreateDto("tester");
+ RacingCarCreateDto actual = RacingCarConverter.toRacingCarCreateDto("tester");
+ assertThat(actual).usingRecursiveComparison().isEqualTo(expected);
+ }
+
+ @Test
+ public void testToRacingCarResultDto() {
+ RacingCarResultDto expected = new RacingCarResultDto("tester", "---", 3);
+ RacingCar racingCar = new RacingCar("tester");
+ racingCar.getResults().addAll(List.of(false, false, true, true, true));
+ RacingCarResultDto actual = RacingCarConverter.toRacingCarResultDto(racingCar);
+ assertThat(actual).usingRecursiveComparison().isEqualTo(expected);
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ public void testResultToString(boolean value) {
+ String expected = value ? "-" : "";
+ String actual = RacingCarConverter.resultToString(value);
+
+ assertThat(actual).isEqualTo(expected);
+ }
+}
diff --git a/src/test/java/RacingCarTest.java b/src/test/java/RacingCarTest.java
new file mode 100644
index 00000000..7b373836
--- /dev/null
+++ b/src/test/java/RacingCarTest.java
@@ -0,0 +1,57 @@
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import converter.RacingCarConverter;
+import dao.RacingCarDao;
+import dto.RacingCarCreateDto;
+import java.util.List;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import service.RacingCarService;
+import service.RacingCarServiceImpl;
+import util.NumberGenerateUtil;
+
+public class RacingCarTest {
+
+ @Nested
+ @DisplayName("자동차 이동 테스트")
+ class RacingCarMoveTest {
+ @ParameterizedTest
+ @ValueSource(ints = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9})
+ @DisplayName("자동차 이동 조건 테스트")
+ void testCondition(int value) {
+ NumberGenerateUtil numberGenerateUtil = mock(NumberGenerateUtil.class);
+ when(numberGenerateUtil.generateRandomNumber()).thenReturn(value);
+ RacingCarDao racingCarDao = new RacingCarDao();
+ RacingCarService racingCarService = new RacingCarServiceImpl(racingCarDao, numberGenerateUtil);
+ List racingCarCreateDtoList = Stream.of("tester1", "tester2").map(v -> RacingCarConverter.toRacingCarCreateDto("")).toList();
+ racingCarService.createRacingCar(racingCarCreateDtoList);
+
+ racingCarService.move();
+
+ int expected = value >= 4 ? 1 : 0;
+ int actual = racingCarService.getResults().get(0).getDistance();
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {-1, 10})
+ @DisplayName("자동차 이동 조건 예외처리 테스트")
+ void moveConditionException(int value) {
+ NumberGenerateUtil numberGenerateUtil = mock(NumberGenerateUtil.class);
+ when(numberGenerateUtil.generateRandomNumber()).thenReturn(value);
+ RacingCarDao racingCarDao = new RacingCarDao();
+ RacingCarService racingCarService = new RacingCarServiceImpl(racingCarDao, numberGenerateUtil);
+ List racingCarCreateDtoList = Stream.of("tester1", "tester2").map(v -> RacingCarConverter.toRacingCarCreateDto("")).toList();
+ racingCarService.createRacingCar(racingCarCreateDtoList);
+
+ assertThatThrownBy(racingCarService::move).isInstanceOf(NumberFormatException.class).hasMessage("0~9 사이의 Power 값을 필요로 합니다.");
+ }
+ }
+}
diff --git a/src/test/java/RacingCarViewTest.java b/src/test/java/RacingCarViewTest.java
new file mode 100644
index 00000000..e04f2eae
--- /dev/null
+++ b/src/test/java/RacingCarViewTest.java
@@ -0,0 +1,64 @@
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.io.ByteArrayInputStream;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import view.RacingCarView;
+import view.RacingCarView.RacingCarInputException;
+
+public class RacingCarViewTest {
+
+ @Nested
+ class InputRacingCarNameTest {
+ @Test
+ @DisplayName("자동차 이름 입력이 하나일 경우 테스트")
+ void inputWithLessThanTwoName() {
+ RacingCarView racingCarView = new RacingCarView();
+ System.setIn(new ByteArrayInputStream("test1\n".getBytes()));
+
+ assertThatThrownBy(racingCarView::inputRacingCarName).isInstanceOf(RacingCarInputException.class).hasMessage("최소한 두 명 이상의 플레이어가 필요합니다.");
+ }
+
+ @Test
+ @DisplayName("자동차 이름에 띄어쓰기가 있을 경우 테스트")
+ void inputWithSpace() {
+ RacingCarView racingCarView = new RacingCarView();
+ System.setIn(new ByteArrayInputStream("test1, test2\n".getBytes()));
+
+ assertThat(racingCarView.inputRacingCarName().stream().noneMatch(s -> s.contains(" "))).isTrue();
+ }
+
+ @Test
+ @DisplayName("자동차 이름에 특수기호 있을 경우 테스트")
+ void inputWithSpecialSymbol() {
+ RacingCarView racingCarView = new RacingCarView();
+ System.setIn(new ByteArrayInputStream("test1#,test2&\n".getBytes()));
+
+ assertThatThrownBy(racingCarView::inputRacingCarName).isInstanceOf(RacingCarInputException.class).hasMessage("이름에 특수기호를 입력할 수 없습니다.");
+ }
+ }
+
+ @Nested
+ class InputRoundTest {
+ @Test
+ @DisplayName("시도할 횟수 입력이 음수일 경우 테스트")
+ void inputWithNegativeNumber() {
+ RacingCarView racingCarView = new RacingCarView();
+ System.setIn(new ByteArrayInputStream("-1\n".getBytes()));
+
+ assertThatThrownBy(racingCarView::inputRound).isInstanceOf(RacingCarInputException.class).hasMessage("횟수는 1회 이상이여야 합니다.");
+ }
+
+ @Test
+ @DisplayName("시도할 횟수 입력이 숫자가 아닌 경우 테스트")
+ void inputWithInvalidCharacter() {
+ RacingCarView racingCarView = new RacingCarView();
+ System.setIn(new ByteArrayInputStream("abc\n".getBytes()));
+
+ assertThatThrownBy(racingCarView::inputRound).isInstanceOf(RacingCarInputException.class).hasMessage("유효하지 않은 입력입니다.");
+ }
+ }
+
+}