From d8044011f9e9097748ff8c14225204c1bb269d95 Mon Sep 17 00:00:00 2001 From: Lee Ye Jin Date: Mon, 27 Oct 2025 13:42:58 +0900 Subject: [PATCH 01/23] =?UTF-8?q?docs(README):=20=EC=9A=94=EA=B5=AC?= =?UTF-8?q?=EC=82=AC=ED=95=AD=20=EB=B0=8F=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EB=AA=A9=EB=A1=9D=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/README.md b/README.md index d0286c859f..4457a287e3 100644 --- a/README.md +++ b/README.md @@ -1 +1,97 @@ # java-racingcar-precourse + +## 자동차 경주 게임 + +초간단 자동차 경주 게임을 구현한다. + +주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다. + +각 자동차에 이름을 부여할 수 있다. 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다. + +자동차 이름은 쉼표(,)를 기준으로 구분하며 이름은 5자 이하만 가능하다. + +사용자는 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다. + +전진하는 조건은 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상일 경우이다. + +경주가 끝나면 가장 멀리 이동한 자동차(들)가 우승하며, 우승자가 여러 명이면 쉼표(,)로 구분하여 출력한다. + +사용자가 잘못된 값을 입력할 경우 `IllegalArgumentException`를 발생시키고, 애플리케이션은 종료되어야 한다. + + +--- + + +# 기능 구현 목록 + +## View + +### 1. 입력 (InputVew) +- [ ] 경주할 자동차 이름을 입력받는다 + - [ ] 입력 요청 문구를 출력한다 + - [ ] 입력된 문자열을 `,` 기준으로 분리한다 + - [ ] 입력된 원시 문자열(List)을 반환한다 + - [ ] 이름은 1자 이상, 5자 이하이다 // + - [ ] 중복된 이름이 있으면 예외를 발생시킨다 // + - [ ] 공백이거나 빈 문자열이면 예외를 발생시킨다 // + +- [ ] 시도할 횟수를 입력받는다 + - [ ] 입력 요청 문구를 출력한다 + - [ ] 입력 문자열을 반환한다 + - [ ] 숫자가 아니면 예외를 발생시킨다 // + - [ ] 0 이하이면 예외를 발생시킨다 // + +### 2. 출력 (OutputView) +- [ ] 각 턴마다 자동차 이름과 이동 거리를 출력한다 +- [ ] 최종 우승자 목록을 전달 받아 출력한다 + - [ ] 우승자가 다수일 경우 `,`로 구분하여 출력한다 + + +## Model + +### 1. 검증 기능 (Validator) +- 입력 형식 및 비즈니스 규칙에 따른 유효성 검증을 담당한다 +- [ ] 문자열 입력값 검증 + - [ ] null, 빈 문자열, 공백 문자열 예외 발생 + - [ ] 자동차 이름이 없는 경우 예외 발생 + - [ ] 이름이 1자 미만 5자 초과일 경우 예외 발생 + - [ ] `,`로 구분된 이름 목록에 공백 요소 존재 . 예외 발생 +- [ ] 자동차 이름 중복일 경우 예외 발생 +- [ ] 시도 횟수 입력값 검증 + - [ ] 숫자가 아닐 경우 예외 발생 + - [ ] 0 이하의 숫자일 경우 예외 발생 + +### 2. 도메인 핵심 로직 (Model) +- [ ] Car 클래스 + - [ ] 자동차 이름과 현재 위치를 가진다 + - [ ] 이동 조건 충족 시 위치를 1 증가시킨다 + - [ ] 상태를 조회할 수 있다 + - [ ] 객체 자신의 유효성을 검증한다 + +- [ ] Cars 일급 컬렉션 + - [ ] private final List + - [ ] 정적 팩토리 메서드로 생성한다 + - [ ] 모든 자동차를 한 번씩 전진시킨다 + - [ ] 최대 이동 거리를 반환한다 + - [ ] 우승자 목록을 반환한다 + - [ ] Car 목록의 이름 중복을 검증한다 + +- [ ] ValueGenerator 인터페이스 + - 랜덤값을 구한다 + - [ ] 구현체는 Randoms.pickNumberInRange(0,9)를 사용해 0~9의 정수를 반환한다 + +- [ ] RacingGame 클래스 + - 게임 전체 진행을 관리한다 + - [ ] 매 라운드마다 이동을 실행한다 + - [ ] 매 라운드마다 결과를 저장하고 반환한다 + - [ ] 게임이 종료된 후 최종 우승자를 계산한다 + + +## Controller + +- [ ] 입력 → 검증 → 모델 실행 → 결과 출력의 전체 흐름을 제어 + - InputView로 입력을 받는다 + - Validator로 형식을 검증한다 + - 유효한 입력값을 사용해 Cars, RacingGame을 초기화한다 + - RacingGame의 결과를 OutputView에 전당해 출력한다 +- 애플리케이션 진입점에서 실행된다 From 633ee25ee3837fa4023a27823b3ea2e621dc0c7c Mon Sep 17 00:00:00 2001 From: Lee Ye Jin Date: Mon, 27 Oct 2025 14:31:29 +0900 Subject: [PATCH 02/23] =?UTF-8?q?feat(model):=20Car=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=ED=81=B4=EB=9E=98=EC=8A=A4,=20=EB=8B=A8=EC=9C=84?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 10 ++-- src/main/java/racingcar/model/car/Car.java | 56 +++++++++++++++++++ .../java/racingcar/model/car/CarTest.java | 42 ++++++++++++++ 3 files changed, 103 insertions(+), 5 deletions(-) create mode 100644 src/main/java/racingcar/model/car/Car.java create mode 100644 src/test/java/racingcar/model/car/CarTest.java diff --git a/README.md b/README.md index 4457a287e3..de12a04b7e 100644 --- a/README.md +++ b/README.md @@ -62,11 +62,11 @@ - [ ] 0 이하의 숫자일 경우 예외 발생 ### 2. 도메인 핵심 로직 (Model) -- [ ] Car 클래스 - - [ ] 자동차 이름과 현재 위치를 가진다 - - [ ] 이동 조건 충족 시 위치를 1 증가시킨다 - - [ ] 상태를 조회할 수 있다 - - [ ] 객체 자신의 유효성을 검증한다 +- [x] Car 클래스 + - [x] 자동차 이름과 현재 위치를 가진다 + - [x] 이동 조건 충족 시 위치를 1 증가시킨다 + - [x] 상태를 조회할 수 있다 + - [x] 객체 자신의 유효성을 검증한다 - [ ] Cars 일급 컬렉션 - [ ] private final List diff --git a/src/main/java/racingcar/model/car/Car.java b/src/main/java/racingcar/model/car/Car.java new file mode 100644 index 0000000000..c50c4880e0 --- /dev/null +++ b/src/main/java/racingcar/model/car/Car.java @@ -0,0 +1,56 @@ +package racingcar.model.car; + +import racingcar.model.constant.ExceptionMessages; +import racingcar.model.generator.ValueGenerator; + +import java.util.Objects; + +public class Car { + + private static final int MOVE_THRESHOLD = 4; + private static final int INITIAL_POSITION = 0; + + private final String name; + private int position = INITIAL_POSITION; + + public Car(String name) { + validateName(name); + this.name = name; + } + + private void validateName(String name) { + if (Objects.isNull(name) || name.isBlank()) { + throw new IllegalArgumentException(ExceptionMessages.INVALID_NAME_EMPTY); + } + if (name.length() > 5) { + throw new IllegalArgumentException(ExceptionMessages.INVALID_NAME_LENGTH); + } + } + + public void move(ValueGenerator generator) { + if (isMovable(generator.getValue())) { + position++; + } + } + + private boolean isMovable(int randomValue) { + return randomValue >= MOVE_THRESHOLD; + } + + public String getName() { + return name; + } + + public int getPosition() { + return position; + } + + public boolean isWinner(int maxPosition) { + return this.position == maxPosition; + } + + @Override + public String toString() { + return name + " : " + "-".repeat(position); + } +} diff --git a/src/test/java/racingcar/model/car/CarTest.java b/src/test/java/racingcar/model/car/CarTest.java new file mode 100644 index 0000000000..28e4fc7a03 --- /dev/null +++ b/src/test/java/racingcar/model/car/CarTest.java @@ -0,0 +1,42 @@ +package racingcar.model.car; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class CarTest { + + @ParameterizedTest + @ValueSource(ints = {4, 5, 6, 9}) + void 값이_4이상이면_전진한다(int value) { + Car car = new Car("pobi"); + car.move(() -> value); + assertThat(car.getPosition()).isEqualTo(1); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2, 3}) + void 값이_4미만이면_이동하지_않는다(int value) { + Car car = new Car("pobi"); + car.move(() -> value); + assertThat(car.getPosition()).isEqualTo(0); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" "}) + void 이름이_null_또는_공백이면_예외(String input) { + assertThatThrownBy(() -> new Car(input)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 이름이_5자초과면_예외() { + assertThatThrownBy(() -> new Car("abcdef")) + .isInstanceOf(IllegalArgumentException.class); + } +} From 1d41b1931cce9ca158bb4a36e338acb140b80ad0 Mon Sep 17 00:00:00 2001 From: Lee Ye Jin Date: Mon, 27 Oct 2025 14:31:44 +0900 Subject: [PATCH 03/23] =?UTF-8?q?feat(model):=20ValueGenerator=20=EC=9D=B8?= =?UTF-8?q?=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/racingcar/model/generator/ValueGenerator.java | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/main/java/racingcar/model/generator/ValueGenerator.java diff --git a/src/main/java/racingcar/model/generator/ValueGenerator.java b/src/main/java/racingcar/model/generator/ValueGenerator.java new file mode 100644 index 0000000000..4a7b6f2c3f --- /dev/null +++ b/src/main/java/racingcar/model/generator/ValueGenerator.java @@ -0,0 +1,6 @@ +package racingcar.model.generator; + +@FunctionalInterface +public interface ValueGenerator { + int getValue(); +} \ No newline at end of file From 1d2052307ddcf2f25c345509fc9e9de90fe431e0 Mon Sep 17 00:00:00 2001 From: Lee Ye Jin Date: Mon, 27 Oct 2025 15:02:28 +0900 Subject: [PATCH 04/23] =?UTF-8?q?feat(model)=20:=20Cars=20=EC=9D=BC?= =?UTF-8?q?=EA=B8=89=20=EC=BB=AC=EB=A0=89=EC=85=98=20=EA=B5=AC=ED=98=84,?= =?UTF-8?q?=20=EB=8B=A8=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 14 ++-- src/main/java/racingcar/model/car/Cars.java | 61 ++++++++++++++++ .../java/racingcar/model/car/CarsTest.java | 73 +++++++++++++++++++ 3 files changed, 141 insertions(+), 7 deletions(-) create mode 100644 src/main/java/racingcar/model/car/Cars.java create mode 100644 src/test/java/racingcar/model/car/CarsTest.java diff --git a/README.md b/README.md index de12a04b7e..0372e2fee6 100644 --- a/README.md +++ b/README.md @@ -68,13 +68,13 @@ - [x] 상태를 조회할 수 있다 - [x] 객체 자신의 유효성을 검증한다 -- [ ] Cars 일급 컬렉션 - - [ ] private final List - - [ ] 정적 팩토리 메서드로 생성한다 - - [ ] 모든 자동차를 한 번씩 전진시킨다 - - [ ] 최대 이동 거리를 반환한다 - - [ ] 우승자 목록을 반환한다 - - [ ] Car 목록의 이름 중복을 검증한다 +- [x] Cars 일급 컬렉션 + - [x] private final List + - [x] 정적 팩토리 메서드로 생성한다 + - [x] 모든 자동차를 한 번씩 전진시킨다 + - [x] 최대 이동 거리를 반환한다 + - [x] 우승자 목록을 반환한다 + - [x] Car 목록의 이름 중복을 검증한다 - [ ] ValueGenerator 인터페이스 - 랜덤값을 구한다 diff --git a/src/main/java/racingcar/model/car/Cars.java b/src/main/java/racingcar/model/car/Cars.java new file mode 100644 index 0000000000..947381e8a5 --- /dev/null +++ b/src/main/java/racingcar/model/car/Cars.java @@ -0,0 +1,61 @@ +package racingcar.model.car; + +import racingcar.model.constant.ExceptionMessages; +import racingcar.model.generator.ValueGenerator; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class Cars { + + private final List cars; + + private Cars(List cars) { + validateDuplicateNames(cars); + this.cars = cars; + } + + public static Cars valueOf(List names) { + if (names == null || names.isEmpty()) { + throw new IllegalArgumentException(ExceptionMessages.INPUT_EMPTY); + } + + List carList = names.stream() + .map(Car::new) + .collect(Collectors.toUnmodifiableList()); + + return new Cars(carList); + } + + private void validateDuplicateNames(List cars) { + Set uniqueNames = cars.stream() + .map(Car::getName) + .collect(Collectors.toSet()); + if (uniqueNames.size() != cars.size()) { + throw new IllegalArgumentException(ExceptionMessages.DUPLICATE_NAME); + } + } + + public void moveAll(ValueGenerator generator) { + cars.forEach(car -> car.move(generator)); + } + + public int getMaxPosition() { + return cars.stream() + .mapToInt(Car::getPosition) + .max() + .orElse(0); + } + + public List getWinners() { + int maxPosition = getMaxPosition(); + return cars.stream() + .filter(car -> car.isWinner(maxPosition)) + .collect(Collectors.toUnmodifiableList()); + } + + public List getCars() { + return List.copyOf(cars); + } +} diff --git a/src/test/java/racingcar/model/car/CarsTest.java b/src/test/java/racingcar/model/car/CarsTest.java new file mode 100644 index 0000000000..9a2f7fcb16 --- /dev/null +++ b/src/test/java/racingcar/model/car/CarsTest.java @@ -0,0 +1,73 @@ +package racingcar.model.car; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import racingcar.model.generator.ValueGenerator; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class CarsTest { + + private List names; + + @BeforeEach + void setUp() { + names = List.of("pobi", "woni", "jun"); + } + + @Test + void 중복된_이름이_있으면_예외() { + List duplicateNames = List.of("pobi", "pobi"); + assertThatThrownBy(() -> Cars.valueOf(duplicateNames)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 모든_자동차가_한번씩_전진한다() { + Cars cars = Cars.valueOf(names); + + ValueGenerator alwaysMove = () -> 9; + cars.moveAll(alwaysMove); + + assertThat(cars.getCars()) + .extracting(Car::getPosition) + .containsExactly(1, 1, 1); + } + + @Test + void 최대_이동_거리를_반환한다() { + Cars cars = Cars.valueOf(names); + + ValueGenerator generator = () -> 9; + cars.moveAll(generator); // 모두 한 번 전진 + cars.moveAll(generator); // 두 번 전진 + + assertThat(cars.getMaxPosition()).isEqualTo(2); + } + + @Test + void 가장_먼_위치의_자동차가_우승자이다() { + Cars cars = Cars.valueOf(names); + + // pobi만 이동 + ValueGenerator moveOnlyPobi = new ValueGenerator() { + private int count = 0; + + @Override + public int getValue() { + return count++ == 0 ? 9 : 0; + } + }; + + cars.moveAll(moveOnlyPobi); + + List winners = cars.getWinners(); + + assertThat(winners) + .extracting(Car::getName) + .containsExactly("pobi"); + } +} From b1bc9b0598152dafada918a08777003999e39c5e Mon Sep 17 00:00:00 2001 From: Lee Ye Jin Date: Mon, 27 Oct 2025 15:11:58 +0900 Subject: [PATCH 05/23] =?UTF-8?q?feat(model):=20RandomValueGenerator=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- .../model/generator/RandomValueGenerator.java | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 src/main/java/racingcar/model/generator/RandomValueGenerator.java diff --git a/README.md b/README.md index 0372e2fee6..cde79fbb11 100644 --- a/README.md +++ b/README.md @@ -76,9 +76,9 @@ - [x] 우승자 목록을 반환한다 - [x] Car 목록의 이름 중복을 검증한다 -- [ ] ValueGenerator 인터페이스 +- [x] ValueGenerator 인터페이스 - 랜덤값을 구한다 - - [ ] 구현체는 Randoms.pickNumberInRange(0,9)를 사용해 0~9의 정수를 반환한다 + - [x] 구현체는 Randoms.pickNumberInRange(0,9)를 사용해 0~9의 정수를 반환한다 - [ ] RacingGame 클래스 - 게임 전체 진행을 관리한다 diff --git a/src/main/java/racingcar/model/generator/RandomValueGenerator.java b/src/main/java/racingcar/model/generator/RandomValueGenerator.java new file mode 100644 index 0000000000..2cfdeba532 --- /dev/null +++ b/src/main/java/racingcar/model/generator/RandomValueGenerator.java @@ -0,0 +1,14 @@ +package racingcar.model.generator; + +import camp.nextstep.edu.missionutils.Randoms; + +public class RandomValueGenerator implements ValueGenerator { + + private static final int MIN = 0; + private static final int MAX = 9; + + @Override + public int getValue() { + return Randoms.pickNumberInRange(MIN, MAX); + } +} \ No newline at end of file From a5f9248e2b447379ac9b9ba8fbb20f9a8ea248d0 Mon Sep 17 00:00:00 2001 From: Lee Ye Jin Date: Mon, 27 Oct 2025 15:55:14 +0900 Subject: [PATCH 06/23] =?UTF-8?q?feat(model):=20RacingGame=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=ED=81=B4=EB=9E=98=EC=8A=A4,=20=EB=8B=A8?= =?UTF-8?q?=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 ++-- .../java/racingcar/model/game/RacingGame.java | 38 +++++++++++++++++++ .../racingcar/model/game/RacingGameTest.java | 36 ++++++++++++++++++ 3 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 src/main/java/racingcar/model/game/RacingGame.java create mode 100644 src/test/java/racingcar/model/game/RacingGameTest.java diff --git a/README.md b/README.md index cde79fbb11..061b7034e6 100644 --- a/README.md +++ b/README.md @@ -80,11 +80,11 @@ - 랜덤값을 구한다 - [x] 구현체는 Randoms.pickNumberInRange(0,9)를 사용해 0~9의 정수를 반환한다 -- [ ] RacingGame 클래스 +- [x] RacingGame 클래스 - 게임 전체 진행을 관리한다 - - [ ] 매 라운드마다 이동을 실행한다 - - [ ] 매 라운드마다 결과를 저장하고 반환한다 - - [ ] 게임이 종료된 후 최종 우승자를 계산한다 + - [x] 매 라운드마다 이동을 실행한다 + - [x] 매 라운드마다 결과를 저장하고 반환한다 + - [x] 게임이 종료된 후 최종 우승자를 계산한다 ## Controller diff --git a/src/main/java/racingcar/model/game/RacingGame.java b/src/main/java/racingcar/model/game/RacingGame.java new file mode 100644 index 0000000000..5e84245abb --- /dev/null +++ b/src/main/java/racingcar/model/game/RacingGame.java @@ -0,0 +1,38 @@ +package racingcar.model.game; + +import racingcar.model.car.Cars; +import racingcar.model.generator.ValueGenerator; + +import java.util.ArrayList; +import java.util.List; + +public class RacingGame { + + private final Cars cars; + private final int attempts; + private final List> roundResults = new ArrayList<>(); + + private RacingGame(Cars cars, int attempts) { + this.cars = cars; + this.attempts = attempts; + } + + public static RacingGame of(Cars cars, int attempts) { + return new RacingGame(cars, attempts); + } + + public void play(ValueGenerator generator) { + for (int i = 0; i < attempts; i++) { + cars.moveAll(generator); + roundResults.add(cars.getStatus()); + } + } + + public List> getRoundResults() { + return new ArrayList<>(roundResults); + } + + public List returnWinners() { + return cars.findWinners(); + } +} \ No newline at end of file diff --git a/src/test/java/racingcar/model/game/RacingGameTest.java b/src/test/java/racingcar/model/game/RacingGameTest.java new file mode 100644 index 0000000000..9486176883 --- /dev/null +++ b/src/test/java/racingcar/model/game/RacingGameTest.java @@ -0,0 +1,36 @@ +package racingcar.model.game; + +import org.junit.jupiter.api.Test; +import racingcar.model.car.Car; +import racingcar.model.car.Cars; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RacingGameTest { + + @Test + void 주어진_횟수만큼_경주를_진행한다() { + Cars cars = Cars.valueOf(List.of("pobi", "woni")); + RacingGame game = RacingGame.of(cars, 3); + + game.play(() -> 5); // 항상 이동 + List> results = game.getRoundResults(); + + assertThat(results).hasSize(3); // 3번 라운드 + assertThat(results.get(2).get(0)).contains("-"); + } + + @Test + void 우승자를_정확히_계산한다() { + Cars cars = Cars.valueOf(List.of("pobi", "woni")); + RacingGame game = RacingGame.of(cars, 1); + + // pobi만 이동 + game.play(() -> 9); + List winners = game.returnWinners(); + + assertThat(winners).contains("pobi"); + } +} \ No newline at end of file From ef6167caa34f4fa3981bbf1fe76663bed47a5878 Mon Sep 17 00:00:00 2001 From: Lee Ye Jin Date: Mon, 27 Oct 2025 15:55:31 +0900 Subject: [PATCH 07/23] =?UTF-8?q?refactor(model):=20Cars=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20getStatus=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80,=20=EC=9A=B0=EC=8A=B9=EC=9E=90=20=EC=82=B0?= =?UTF-8?q?=EC=B6=9C=20=EB=A9=94=EC=84=9C=EB=93=9C=EB=AA=85=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD,=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/racingcar/model/car/Cars.java | 9 ++++++++- src/test/java/racingcar/model/car/CarsTest.java | 5 ++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/main/java/racingcar/model/car/Cars.java b/src/main/java/racingcar/model/car/Cars.java index 947381e8a5..4fef99c89a 100644 --- a/src/main/java/racingcar/model/car/Cars.java +++ b/src/main/java/racingcar/model/car/Cars.java @@ -48,10 +48,17 @@ public int getMaxPosition() { .orElse(0); } - public List getWinners() { + public List findWinners() { int maxPosition = getMaxPosition(); return cars.stream() .filter(car -> car.isWinner(maxPosition)) + .map(Car::getName) + .collect(Collectors.toUnmodifiableList()); + } + + public List getStatus() { + return cars.stream() + .map(Car::toString) .collect(Collectors.toUnmodifiableList()); } diff --git a/src/test/java/racingcar/model/car/CarsTest.java b/src/test/java/racingcar/model/car/CarsTest.java index 9a2f7fcb16..13a5888290 100644 --- a/src/test/java/racingcar/model/car/CarsTest.java +++ b/src/test/java/racingcar/model/car/CarsTest.java @@ -64,10 +64,9 @@ public int getValue() { cars.moveAll(moveOnlyPobi); - List winners = cars.getWinners(); + List winners = cars.findWinners(); assertThat(winners) - .extracting(Car::getName) .containsExactly("pobi"); } -} +} \ No newline at end of file From 16597efb6245f05cf7283bcfcc1e6139eb9ca702 Mon Sep 17 00:00:00 2001 From: Lee Ye Jin Date: Mon, 27 Oct 2025 16:30:01 +0900 Subject: [PATCH 08/23] =?UTF-8?q?feat(view):=20InputView=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=20=EC=9E=85=EB=A0=A5=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 21 ++++++---------- src/main/java/racingcar/view/InputView.java | 28 +++++++++++++++++++++ 2 files changed, 36 insertions(+), 13 deletions(-) create mode 100644 src/main/java/racingcar/view/InputView.java diff --git a/README.md b/README.md index 061b7034e6..3c2aa24202 100644 --- a/README.md +++ b/README.md @@ -27,19 +27,14 @@ ## View ### 1. 입력 (InputVew) -- [ ] 경주할 자동차 이름을 입력받는다 - - [ ] 입력 요청 문구를 출력한다 - - [ ] 입력된 문자열을 `,` 기준으로 분리한다 - - [ ] 입력된 원시 문자열(List)을 반환한다 - - [ ] 이름은 1자 이상, 5자 이하이다 // - - [ ] 중복된 이름이 있으면 예외를 발생시킨다 // - - [ ] 공백이거나 빈 문자열이면 예외를 발생시킨다 // - -- [ ] 시도할 횟수를 입력받는다 - - [ ] 입력 요청 문구를 출력한다 - - [ ] 입력 문자열을 반환한다 - - [ ] 숫자가 아니면 예외를 발생시킨다 // - - [ ] 0 이하이면 예외를 발생시킨다 // +- [x] 경주할 자동차 이름을 입력받는다 + - [x] 입력 요청 문구를 출력한다 + - [x] 입력된 문자열을 `,` 기준으로 분리한다 + - [x] 입력된 원시 문자열(List)을 반환한다 + +- [x] 시도할 횟수를 입력받는다 + - [x] 입력 요청 문구를 출력한다 + - [x] 입력 문자열을 반환한다 ### 2. 출력 (OutputView) - [ ] 각 턴마다 자동차 이름과 이동 거리를 출력한다 diff --git a/src/main/java/racingcar/view/InputView.java b/src/main/java/racingcar/view/InputView.java new file mode 100644 index 0000000000..b3fbad9620 --- /dev/null +++ b/src/main/java/racingcar/view/InputView.java @@ -0,0 +1,28 @@ +package racingcar.view; + +import camp.nextstep.edu.missionutils.Console; +import java.util.Arrays; +import java.util.List; + +public class InputView { + + private static final String INPUT_CAR_NAMES_MESSAGE = "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"; + private static final String INPUT_ATTEMPTS_MESSAGE = "시도할 횟수는 몇 회인가요?"; + + private InputView() { + } + + public static List readCarNames() { + System.out.println(INPUT_CAR_NAMES_MESSAGE); + String input = Console.readLine(); + + return Arrays.stream(input.split(",")) + .map(String::trim) + .toList(); + } + + public static String readAttemptCount() { + System.out.println(INPUT_ATTEMPTS_MESSAGE); + return Console.readLine(); + } +} \ No newline at end of file From 5e2fb4277df0054809786f4f20be464f1d17bb6f Mon Sep 17 00:00:00 2001 From: Lee Ye Jin Date: Mon, 27 Oct 2025 16:31:40 +0900 Subject: [PATCH 09/23] =?UTF-8?q?feat(model):=20=EC=9E=85=EB=A0=A5?= =?UTF-8?q?=EA=B0=92=20=EA=B2=80=EC=A6=9D=EC=9D=84=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?Validator=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EA=B5=AC=ED=98=84,?= =?UTF-8?q?=20=EB=8B=A8=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 18 ++--- src/main/java/racingcar/model/Validator.java | 51 +++++++++++++ .../java/racingcar/model/ValidatorTest.java | 71 +++++++++++++++++++ 3 files changed, 131 insertions(+), 9 deletions(-) create mode 100644 src/main/java/racingcar/model/Validator.java create mode 100644 src/test/java/racingcar/model/ValidatorTest.java diff --git a/README.md b/README.md index 3c2aa24202..e980f3bb64 100644 --- a/README.md +++ b/README.md @@ -46,15 +46,15 @@ ### 1. 검증 기능 (Validator) - 입력 형식 및 비즈니스 규칙에 따른 유효성 검증을 담당한다 -- [ ] 문자열 입력값 검증 - - [ ] null, 빈 문자열, 공백 문자열 예외 발생 - - [ ] 자동차 이름이 없는 경우 예외 발생 - - [ ] 이름이 1자 미만 5자 초과일 경우 예외 발생 - - [ ] `,`로 구분된 이름 목록에 공백 요소 존재 . 예외 발생 -- [ ] 자동차 이름 중복일 경우 예외 발생 -- [ ] 시도 횟수 입력값 검증 - - [ ] 숫자가 아닐 경우 예외 발생 - - [ ] 0 이하의 숫자일 경우 예외 발생 +- [x] 문자열 입력값 검증 + - [x] null, 빈 문자열, 공백 문자열 예외 발생 + - [x] 자동차 이름이 없는 경우 예외 발생 + - [x] 이름이 1자 미만 5자 초과일 경우 예외 발생 + - [x] `,`로 구분된 이름 목록에 공백 요소 존재 . 예외 발생 +- [x] 자동차 이름 중복일 경우 예외 발생 +- [x] 시도 횟수 입력값 검증 + - [x] 숫자가 아닐 경우 예외 발생 + - [x] 0 이하의 숫자일 경우 예외 발생 ### 2. 도메인 핵심 로직 (Model) - [x] Car 클래스 diff --git a/src/main/java/racingcar/model/Validator.java b/src/main/java/racingcar/model/Validator.java new file mode 100644 index 0000000000..7a7973917e --- /dev/null +++ b/src/main/java/racingcar/model/Validator.java @@ -0,0 +1,51 @@ +package racingcar.model; + +import racingcar.model.constant.ExceptionMessages; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class Validator { + + private Validator() { + } + + public static void validateNames(List names) { + if (names == null || names.isEmpty()) { + throw new IllegalArgumentException(ExceptionMessages.INPUT_EMPTY); + } + + for (String name : names) { + if (name == null || name.isBlank()) { + throw new IllegalArgumentException(ExceptionMessages.INPUT_EMPTY); + } + if (name.length() > 5) { + throw new IllegalArgumentException(ExceptionMessages.INVALID_NAME_LENGTH); + } + } + + Set unique = new HashSet<>(names); + if (unique.size() != names.size()) { + throw new IllegalArgumentException(ExceptionMessages.DUPLICATE_NAME); + } + } + + public static int validateAttempts(String input) { + if (input == null || input.isBlank()) { + throw new IllegalArgumentException(ExceptionMessages.INPUT_EMPTY); + } + + int count; + try { + count = Integer.parseInt(input); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(ExceptionMessages.INVALID_NUMBER_FORMAT); + } + + if (count <= 0) { + throw new IllegalArgumentException(ExceptionMessages.INVALID_NUMBER_RANGE); + } + + return count; + } +} \ No newline at end of file diff --git a/src/test/java/racingcar/model/ValidatorTest.java b/src/test/java/racingcar/model/ValidatorTest.java new file mode 100644 index 0000000000..173e3e1a5b --- /dev/null +++ b/src/test/java/racingcar/model/ValidatorTest.java @@ -0,0 +1,71 @@ +package racingcar.model; + +import org.junit.jupiter.api.Test; +import racingcar.model.constant.ExceptionMessages; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.assertThat; + +class ValidatorTest { + + @Test + void 이름이_null이거나_빈문자열이면_예외() { + assertThatThrownBy(() -> Validator.validateNames(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessages.INPUT_EMPTY); + + assertThatThrownBy(() -> Validator.validateNames(List.of())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessages.INPUT_EMPTY); + } + + @Test + void 이름이_공백이거나_5자초과이면_예외() { + assertThatThrownBy(() -> Validator.validateNames(List.of(" "))) + .isInstanceOf(IllegalArgumentException.class); + + assertThatThrownBy(() -> Validator.validateNames(List.of("abcdef"))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessages.INVALID_NAME_LENGTH); + } + + @Test + void 이름이_중복이면_예외() { + assertThatThrownBy(() -> Validator.validateNames(List.of("pobi", "pobi"))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessages.DUPLICATE_NAME); + } + + @Test + void 시도횟수가_null이거나_공백이면_예외() { + assertThatThrownBy(() -> Validator.validateAttempts(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessages.INPUT_EMPTY); + + assertThatThrownBy(() -> Validator.validateAttempts(" ")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessages.INPUT_EMPTY); + } + + @Test + void 시도횟수가_숫자가_아니면_예외() { + assertThatThrownBy(() -> Validator.validateAttempts("abc")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessages.INVALID_NUMBER_FORMAT); + } + + @Test + void 시도횟수가_0이하면_예외() { + assertThatThrownBy(() -> Validator.validateAttempts("0")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessages.INVALID_NUMBER_RANGE); + } + + @Test + void 시도횟수가_유효하면_정수로_반환한다() { + int count = Validator.validateAttempts("3"); + assertThat(count).isEqualTo(3); + } +} \ No newline at end of file From 9614b3e7cc28f306532a0c21bbd1adfe7bed4d9f Mon Sep 17 00:00:00 2001 From: Lee Ye Jin Date: Mon, 27 Oct 2025 16:33:25 +0900 Subject: [PATCH 10/23] =?UTF-8?q?refactor(constant):=20Validator=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=20=EA=B7=9C=EC=B9=99?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EC=98=88=EC=99=B8=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EC=83=81=EC=88=98=EB=AA=85=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/constant/ExceptionMessages.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/main/java/racingcar/model/constant/ExceptionMessages.java diff --git a/src/main/java/racingcar/model/constant/ExceptionMessages.java b/src/main/java/racingcar/model/constant/ExceptionMessages.java new file mode 100644 index 0000000000..8144b07a7f --- /dev/null +++ b/src/main/java/racingcar/model/constant/ExceptionMessages.java @@ -0,0 +1,20 @@ +package racingcar.model.constant; + +public final class ExceptionMessages { + + private ExceptionMessages() {} + + // 공통 입력 예외 + public static final String INPUT_EMPTY = "입력값이 비어있습니다."; + public static final String INPUT_NULL = "입력값이 존재하지 않습니다."; + public static final String INPUT_BLANK = "입력값에 공백만 포함될 수 없습니다."; + + // 자동차 이름 예외 + public static final String INVALID_NAME_EMPTY = "자동차 이름은 비어있을 수 없습니다."; + public static final String INVALID_NAME_LENGTH = "자동차 이름은 1자 이상 5자 이하만 가능합니다."; + public static final String DUPLICATE_NAME = "자동차 이름은 중복될 수 없습니다."; + + // 시도 횟수 관련 예외 + public static final String INVALID_NUMBER_FORMAT = "시도 횟수는 숫자여야 합니다."; + public static final String INVALID_NUMBER_RANGE = "시도 횟수는 1 이상의 정수여야 합니다."; +} From 75cdba505653162ace3bb207657aa8eb6639e098 Mon Sep 17 00:00:00 2001 From: Lee Ye Jin Date: Mon, 27 Oct 2025 16:36:57 +0900 Subject: [PATCH 11/23] =?UTF-8?q?feat(view):=20OutputView=20=EB=9D=BC?= =?UTF-8?q?=EC=9A=B4=EB=93=9C=20=EA=B2=B0=EA=B3=BC=20=EB=B0=8F=20=EC=9A=B0?= =?UTF-8?q?=EC=8A=B9=EC=9E=90=20=EC=B6=9C=EB=A0=A5=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 ++--- src/main/java/racingcar/view/OutputView.java | 24 ++++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 src/main/java/racingcar/view/OutputView.java diff --git a/README.md b/README.md index e980f3bb64..29987860be 100644 --- a/README.md +++ b/README.md @@ -37,9 +37,9 @@ - [x] 입력 문자열을 반환한다 ### 2. 출력 (OutputView) -- [ ] 각 턴마다 자동차 이름과 이동 거리를 출력한다 -- [ ] 최종 우승자 목록을 전달 받아 출력한다 - - [ ] 우승자가 다수일 경우 `,`로 구분하여 출력한다 +- [x] 각 턴마다 자동차 이름과 이동 거리를 출력한다 +- [x] 최종 우승자 목록을 전달 받아 출력한다 + - [x] 우승자가 다수일 경우 `,`로 구분하여 출력한다 ## Model diff --git a/src/main/java/racingcar/view/OutputView.java b/src/main/java/racingcar/view/OutputView.java new file mode 100644 index 0000000000..ea5aa15a13 --- /dev/null +++ b/src/main/java/racingcar/view/OutputView.java @@ -0,0 +1,24 @@ +package racingcar.view; + +import java.util.List; + +public class OutputView { + + private static final String RESULT_HEADER = "실행 결과"; + private static final String WINNER_ANNOUNCEMENT = "최종 우승자 : "; + + public void printResultHeader() { + System.out.println(RESULT_HEADER); + } + + public void printRoundResult(List> roundResults) { + for (List round : roundResults) { + round.forEach(System.out::println); + System.out.println(); // 라운드 구분용 공백 줄 + } + } + + public void printWinners(List winners) { + System.out.println(WINNER_ANNOUNCEMENT + String.join(", ", winners)); + } +} From 3f741709795ea5d5e0d1848cb335e98df4c8f049 Mon Sep 17 00:00:00 2001 From: Lee Ye Jin Date: Mon, 27 Oct 2025 16:58:57 +0900 Subject: [PATCH 12/23] =?UTF-8?q?refactor(view,=20model):=20InputView,=20V?= =?UTF-8?q?alidator=EC=9D=98=20static=20=EC=82=AD=EC=A0=9C,=20=EC=9D=B8?= =?UTF-8?q?=EC=8A=A4=ED=84=B4=EC=8A=A4=20=EA=B8=B0=EB=B0=98=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/racingcar/model/Validator.java | 8 ++++---- src/main/java/racingcar/view/InputView.java | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/racingcar/model/Validator.java b/src/main/java/racingcar/model/Validator.java index 7a7973917e..1a06e15602 100644 --- a/src/main/java/racingcar/model/Validator.java +++ b/src/main/java/racingcar/model/Validator.java @@ -7,10 +7,10 @@ public class Validator { - private Validator() { + public Validator() { } - public static void validateNames(List names) { + public void validateNames(List names) { if (names == null || names.isEmpty()) { throw new IllegalArgumentException(ExceptionMessages.INPUT_EMPTY); } @@ -30,7 +30,7 @@ public static void validateNames(List names) { } } - public static int validateAttempts(String input) { + public int validateAttempts(String input) { if (input == null || input.isBlank()) { throw new IllegalArgumentException(ExceptionMessages.INPUT_EMPTY); } @@ -48,4 +48,4 @@ public static int validateAttempts(String input) { return count; } -} \ No newline at end of file +} diff --git a/src/main/java/racingcar/view/InputView.java b/src/main/java/racingcar/view/InputView.java index b3fbad9620..333c117c72 100644 --- a/src/main/java/racingcar/view/InputView.java +++ b/src/main/java/racingcar/view/InputView.java @@ -9,10 +9,10 @@ public class InputView { private static final String INPUT_CAR_NAMES_MESSAGE = "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"; private static final String INPUT_ATTEMPTS_MESSAGE = "시도할 횟수는 몇 회인가요?"; - private InputView() { + public InputView() { } - public static List readCarNames() { + public List readCarNames() { System.out.println(INPUT_CAR_NAMES_MESSAGE); String input = Console.readLine(); @@ -21,8 +21,8 @@ public static List readCarNames() { .toList(); } - public static String readAttemptCount() { + public String readAttemptCount() { System.out.println(INPUT_ATTEMPTS_MESSAGE); return Console.readLine(); } -} \ No newline at end of file +} From 1aed89cc55eba8d7d608003c26e6cba7f67d43f6 Mon Sep 17 00:00:00 2001 From: Lee Ye Jin Date: Mon, 27 Oct 2025 16:59:26 +0900 Subject: [PATCH 13/23] =?UTF-8?q?test(validator):=20Validator=20=EC=9D=B8?= =?UTF-8?q?=EC=8A=A4=ED=84=B4=EC=8A=A4=20=EA=B8=B0=EB=B0=98=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EB=8B=A8=EC=9C=84=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/racingcar/model/ValidatorTest.java | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/test/java/racingcar/model/ValidatorTest.java b/src/test/java/racingcar/model/ValidatorTest.java index 173e3e1a5b..8c17a9d113 100644 --- a/src/test/java/racingcar/model/ValidatorTest.java +++ b/src/test/java/racingcar/model/ValidatorTest.java @@ -10,62 +10,65 @@ class ValidatorTest { + private final Validator validator = new Validator(); + @Test void 이름이_null이거나_빈문자열이면_예외() { - assertThatThrownBy(() -> Validator.validateNames(null)) + assertThatThrownBy(() -> validator.validateNames(null)) .isInstanceOf(IllegalArgumentException.class) .hasMessage(ExceptionMessages.INPUT_EMPTY); - assertThatThrownBy(() -> Validator.validateNames(List.of())) + assertThatThrownBy(() -> validator.validateNames(List.of())) .isInstanceOf(IllegalArgumentException.class) .hasMessage(ExceptionMessages.INPUT_EMPTY); } @Test void 이름이_공백이거나_5자초과이면_예외() { - assertThatThrownBy(() -> Validator.validateNames(List.of(" "))) - .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> validator.validateNames(List.of(" "))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessages.INPUT_EMPTY); - assertThatThrownBy(() -> Validator.validateNames(List.of("abcdef"))) + assertThatThrownBy(() -> validator.validateNames(List.of("abcdef"))) .isInstanceOf(IllegalArgumentException.class) .hasMessage(ExceptionMessages.INVALID_NAME_LENGTH); } @Test void 이름이_중복이면_예외() { - assertThatThrownBy(() -> Validator.validateNames(List.of("pobi", "pobi"))) + assertThatThrownBy(() -> validator.validateNames(List.of("pobi", "pobi"))) .isInstanceOf(IllegalArgumentException.class) .hasMessage(ExceptionMessages.DUPLICATE_NAME); } @Test void 시도횟수가_null이거나_공백이면_예외() { - assertThatThrownBy(() -> Validator.validateAttempts(null)) + assertThatThrownBy(() -> validator.validateAttempts(null)) .isInstanceOf(IllegalArgumentException.class) .hasMessage(ExceptionMessages.INPUT_EMPTY); - assertThatThrownBy(() -> Validator.validateAttempts(" ")) + assertThatThrownBy(() -> validator.validateAttempts(" ")) .isInstanceOf(IllegalArgumentException.class) .hasMessage(ExceptionMessages.INPUT_EMPTY); } @Test void 시도횟수가_숫자가_아니면_예외() { - assertThatThrownBy(() -> Validator.validateAttempts("abc")) + assertThatThrownBy(() -> validator.validateAttempts("abc")) .isInstanceOf(IllegalArgumentException.class) .hasMessage(ExceptionMessages.INVALID_NUMBER_FORMAT); } @Test void 시도횟수가_0이하면_예외() { - assertThatThrownBy(() -> Validator.validateAttempts("0")) + assertThatThrownBy(() -> validator.validateAttempts("0")) .isInstanceOf(IllegalArgumentException.class) .hasMessage(ExceptionMessages.INVALID_NUMBER_RANGE); } @Test void 시도횟수가_유효하면_정수로_반환한다() { - int count = Validator.validateAttempts("3"); + int count = validator.validateAttempts("3"); assertThat(count).isEqualTo(3); } } \ No newline at end of file From 3037a2b78babea1cfe5a129222f73f5304e3e949 Mon Sep 17 00:00:00 2001 From: Lee Ye Jin Date: Mon, 27 Oct 2025 16:59:57 +0900 Subject: [PATCH 14/23] =?UTF-8?q?feat(controller):=20GameController=20?= =?UTF-8?q?=EB=B0=8F=20Application=20=EA=B5=AC=EC=84=B1,=20=EA=B2=8C?= =?UTF-8?q?=EC=9E=84=20=EC=8B=A4=ED=96=89=20=ED=9D=90=EB=A6=84=20=EC=99=84?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/main/java/racingcar/Application.java | 17 ++++++- .../racingcar/controller/GameController.java | 45 +++++++++++++++++++ 3 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 src/main/java/racingcar/controller/GameController.java diff --git a/README.md b/README.md index 29987860be..49f9616d61 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ ## Controller -- [ ] 입력 → 검증 → 모델 실행 → 결과 출력의 전체 흐름을 제어 +- [x] 입력 → 검증 → 모델 실행 → 결과 출력의 전체 흐름을 제어 - InputView로 입력을 받는다 - Validator로 형식을 검증한다 - 유효한 입력값을 사용해 Cars, RacingGame을 초기화한다 diff --git a/src/main/java/racingcar/Application.java b/src/main/java/racingcar/Application.java index a17a52e724..e5ac627514 100644 --- a/src/main/java/racingcar/Application.java +++ b/src/main/java/racingcar/Application.java @@ -1,7 +1,20 @@ package racingcar; +import racingcar.controller.GameController; +import racingcar.model.Validator; +import racingcar.model.generator.RandomValueGenerator; +import racingcar.model.generator.ValueGenerator; +import racingcar.view.InputView; +import racingcar.view.OutputView; + public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + InputView inputView = new InputView(); + OutputView outputView = new OutputView(); + Validator validator = new Validator(); + ValueGenerator generator = new RandomValueGenerator(); + + GameController controller = new GameController(inputView, outputView, validator, generator); + controller.run(); } -} +} \ No newline at end of file diff --git a/src/main/java/racingcar/controller/GameController.java b/src/main/java/racingcar/controller/GameController.java new file mode 100644 index 0000000000..974133b09e --- /dev/null +++ b/src/main/java/racingcar/controller/GameController.java @@ -0,0 +1,45 @@ +package racingcar.controller; + +import racingcar.model.car.Cars; +import racingcar.model.game.RacingGame; +import racingcar.model.generator.ValueGenerator; +import racingcar.model.Validator; +import racingcar.view.InputView; +import racingcar.view.OutputView; + +import java.util.List; + +public class GameController { + private final InputView inputView; + private final OutputView outputView; + private final Validator validator; + private final ValueGenerator generator; + + public GameController( + InputView inputView, + OutputView outputView, + Validator validator, + ValueGenerator generator + ) { + this.inputView = inputView; + this.outputView = outputView; + this.validator = validator; + this.generator = generator; + } + + public void run() { + List names = inputView.readCarNames(); + validator.validateNames(names); + + String attemptsInput = inputView.readAttemptCount(); + int attempts = validator.validateAttempts(attemptsInput); + + Cars cars = Cars.valueOf(names); + RacingGame game = RacingGame.of(cars, attempts); + + outputView.printResultHeader(); + game.play(generator); + outputView.printRoundResult(game.getRoundResults()); + outputView.printWinners(game.returnWinners()); + } +} \ No newline at end of file From 37032d168ce656624c53fb6366a0a1b75aa05a72 Mon Sep 17 00:00:00 2001 From: Lee Ye Jin Date: Mon, 27 Oct 2025 19:39:05 +0900 Subject: [PATCH 15/23] =?UTF-8?q?test:=20RacingGame=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80,=20=EA=B9=8A=EC=9D=80=20?= =?UTF-8?q?=EB=B3=B5=EC=82=AC=20=EB=A1=9C=EC=A7=81=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=B4=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/racingcar/model/game/RacingGame.java | 8 +++- .../racingcar/model/game/RacingGameTest.java | 46 +++++++++++++++---- 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/src/main/java/racingcar/model/game/RacingGame.java b/src/main/java/racingcar/model/game/RacingGame.java index 5e84245abb..338e58553e 100644 --- a/src/main/java/racingcar/model/game/RacingGame.java +++ b/src/main/java/racingcar/model/game/RacingGame.java @@ -5,6 +5,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; public class RacingGame { @@ -18,6 +19,9 @@ private RacingGame(Cars cars, int attempts) { } public static RacingGame of(Cars cars, int attempts) { + if (attempts <= 0) { + throw new IllegalArgumentException("시도 횟수는 1 이상이어야 합니다."); + } return new RacingGame(cars, attempts); } @@ -29,7 +33,9 @@ public void play(ValueGenerator generator) { } public List> getRoundResults() { - return new ArrayList<>(roundResults); + return roundResults.stream() + .map(inner -> new ArrayList<>(inner)) + .collect(Collectors.toList()); } public List returnWinners() { diff --git a/src/test/java/racingcar/model/game/RacingGameTest.java b/src/test/java/racingcar/model/game/RacingGameTest.java index 9486176883..9e85a07dfc 100644 --- a/src/test/java/racingcar/model/game/RacingGameTest.java +++ b/src/test/java/racingcar/model/game/RacingGameTest.java @@ -1,36 +1,62 @@ package racingcar.model.game; import org.junit.jupiter.api.Test; -import racingcar.model.car.Car; import racingcar.model.car.Cars; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; -public class RacingGameTest { +class RacingGameTest { @Test void 주어진_횟수만큼_경주를_진행한다() { Cars cars = Cars.valueOf(List.of("pobi", "woni")); RacingGame game = RacingGame.of(cars, 3); - game.play(() -> 5); // 항상 이동 + game.play(() -> 5); List> results = game.getRoundResults(); - assertThat(results).hasSize(3); // 3번 라운드 + assertThat(results).hasSize(3); assertThat(results.get(2).get(0)).contains("-"); } @Test - void 우승자를_정확히_계산한다() { - Cars cars = Cars.valueOf(List.of("pobi", "woni")); + void 시도_횟수가_0이면_예외() { + Cars cars = Cars.valueOf(List.of("pobi")); + assertThatThrownBy(() -> RacingGame.of(cars, 0)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 시도_횟수가_음수면_예외() { + Cars cars = Cars.valueOf(List.of("pobi")); + assertThatThrownBy(() -> RacingGame.of(cars, -1)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 라운드_결과는_깊은_복사이다() { + Cars cars = Cars.valueOf(List.of("pobi")); RacingGame game = RacingGame.of(cars, 1); - // pobi만 이동 game.play(() -> 9); - List winners = game.returnWinners(); - assertThat(winners).contains("pobi"); + List> firstCall = game.getRoundResults(); + List> secondCall = game.getRoundResults(); + + assertThat(firstCall).isNotSameAs(secondCall); + + assertThat(firstCall.get(0)).isNotSameAs(secondCall.get(0)); + } + + @Test + void 최종_우승자를_반환한다() { + Cars cars = Cars.valueOf(List.of("pobi", "woni")); + RacingGame game = RacingGame.of(cars, 1); + game.play(() -> 9); + List winners = game.returnWinners(); + assertThat(winners).containsExactlyInAnyOrder("pobi", "woni"); } -} \ No newline at end of file +} From ffd8d6023f066f86e45447d7cdb06c3d9667455d Mon Sep 17 00:00:00 2001 From: Lee Ye Jin Date: Mon, 27 Oct 2025 19:39:52 +0900 Subject: [PATCH 16/23] =?UTF-8?q?test:=20Car=20=EC=9D=B4=EB=8F=99/?= =?UTF-8?q?=EC=A0=95=EC=A7=80=20=EC=A1=B0=EA=B1=B4,=20=EC=B4=88=EA=B8=B0?= =?UTF-8?q?=20=EC=83=81=ED=83=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/java/racingcar/model/car/CarTest.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/java/racingcar/model/car/CarTest.java b/src/test/java/racingcar/model/car/CarTest.java index 28e4fc7a03..5308a2e41a 100644 --- a/src/test/java/racingcar/model/car/CarTest.java +++ b/src/test/java/racingcar/model/car/CarTest.java @@ -8,11 +8,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -public class CarTest { +class CarTest { @ParameterizedTest @ValueSource(ints = {4, 5, 6, 9}) - void 값이_4이상이면_전진한다(int value) { + void 랜덤값이_4_이상이면_전진한다(int value) { Car car = new Car("pobi"); car.move(() -> value); assertThat(car.getPosition()).isEqualTo(1); @@ -20,10 +20,10 @@ public class CarTest { @ParameterizedTest @ValueSource(ints = {0, 1, 2, 3}) - void 값이_4미만이면_이동하지_않는다(int value) { + void 랜덤값이_4_미만이면_이동하지_않는다(int value) { Car car = new Car("pobi"); car.move(() -> value); - assertThat(car.getPosition()).isEqualTo(0); + assertThat(car.getPosition()).isZero(); } @ParameterizedTest From aa5c9e4fce1b98912e791e99f9f0585f330b4f32 Mon Sep 17 00:00:00 2001 From: Lee Ye Jin Date: Mon, 27 Oct 2025 19:40:08 +0900 Subject: [PATCH 17/23] =?UTF-8?q?test:=20Cars=20=EC=A0=84=EC=B2=B4=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99,=20=EC=9A=B0=EC=8A=B9=EC=9E=90=20=EC=84=A0?= =?UTF-8?q?=EB=B3=84=20=EB=A1=9C=EC=A7=81=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/racingcar/model/car/CarsTest.java | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/test/java/racingcar/model/car/CarsTest.java b/src/test/java/racingcar/model/car/CarsTest.java index 13a5888290..ae34d1f073 100644 --- a/src/test/java/racingcar/model/car/CarsTest.java +++ b/src/test/java/racingcar/model/car/CarsTest.java @@ -25,13 +25,23 @@ void setUp() { .isInstanceOf(IllegalArgumentException.class); } + @Test + void 빈_리스트면_예외() { + assertThatThrownBy(() -> Cars.valueOf(List.of())) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void null_입력이면_예외() { + assertThatThrownBy(() -> Cars.valueOf(null)) + .isInstanceOf(IllegalArgumentException.class); + } + @Test void 모든_자동차가_한번씩_전진한다() { Cars cars = Cars.valueOf(names); - ValueGenerator alwaysMove = () -> 9; cars.moveAll(alwaysMove); - assertThat(cars.getCars()) .extracting(Car::getPosition) .containsExactly(1, 1, 1); @@ -40,7 +50,6 @@ void setUp() { @Test void 최대_이동_거리를_반환한다() { Cars cars = Cars.valueOf(names); - ValueGenerator generator = () -> 9; cars.moveAll(generator); // 모두 한 번 전진 cars.moveAll(generator); // 두 번 전진 @@ -48,24 +57,26 @@ void setUp() { assertThat(cars.getMaxPosition()).isEqualTo(2); } + @Test + void 공동_우승자가_존재할_수_있다() { + Cars cars = Cars.valueOf(List.of("pobi", "woni")); + cars.moveAll(() -> 9); + List winners = cars.findWinners(); + assertThat(winners).containsExactlyInAnyOrder("pobi", "woni"); + } + @Test void 가장_먼_위치의_자동차가_우승자이다() { Cars cars = Cars.valueOf(names); - - // pobi만 이동 ValueGenerator moveOnlyPobi = new ValueGenerator() { private int count = 0; - @Override public int getValue() { return count++ == 0 ? 9 : 0; } }; - cars.moveAll(moveOnlyPobi); - List winners = cars.findWinners(); - assertThat(winners) .containsExactly("pobi"); } From 6e5af97455c224c1564dfb31ed514ca4038c5551 Mon Sep 17 00:00:00 2001 From: Lee Ye Jin Date: Mon, 27 Oct 2025 19:51:43 +0900 Subject: [PATCH 18/23] =?UTF-8?q?refactor(view):=20CarStatus=20DTO=20?= =?UTF-8?q?=EB=8F=84=EC=9E=85=20=EB=B0=8F=20=EC=B6=9C=EB=A0=A5=20=EC=B1=85?= =?UTF-8?q?=EC=9E=84=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../racingcar/controller/GameController.java | 7 +++-- src/main/java/racingcar/model/car/Car.java | 5 ++-- .../java/racingcar/model/car/CarStatus.java | 3 +++ src/main/java/racingcar/model/car/Cars.java | 6 ++--- .../java/racingcar/model/game/RacingGame.java | 27 +++++++++++++------ src/main/java/racingcar/view/OutputView.java | 10 ++++--- 6 files changed, 38 insertions(+), 20 deletions(-) create mode 100644 src/main/java/racingcar/model/car/CarStatus.java diff --git a/src/main/java/racingcar/controller/GameController.java b/src/main/java/racingcar/controller/GameController.java index 974133b09e..91b951d02b 100644 --- a/src/main/java/racingcar/controller/GameController.java +++ b/src/main/java/racingcar/controller/GameController.java @@ -38,8 +38,11 @@ public void run() { RacingGame game = RacingGame.of(cars, attempts); outputView.printResultHeader(); - game.play(generator); - outputView.printRoundResult(game.getRoundResults()); + + for (int i = 0; i < attempts; i++) { + game.playRound(generator); + outputView.printRoundResult(game.getCurrentRoundSnapshots()); + } outputView.printWinners(game.returnWinners()); } } \ No newline at end of file diff --git a/src/main/java/racingcar/model/car/Car.java b/src/main/java/racingcar/model/car/Car.java index c50c4880e0..bc9cc454ea 100644 --- a/src/main/java/racingcar/model/car/Car.java +++ b/src/main/java/racingcar/model/car/Car.java @@ -49,8 +49,7 @@ public boolean isWinner(int maxPosition) { return this.position == maxPosition; } - @Override - public String toString() { - return name + " : " + "-".repeat(position); + public CarStatus snapshot() { + return new CarStatus(name, position); } } diff --git a/src/main/java/racingcar/model/car/CarStatus.java b/src/main/java/racingcar/model/car/CarStatus.java new file mode 100644 index 0000000000..919e6956a8 --- /dev/null +++ b/src/main/java/racingcar/model/car/CarStatus.java @@ -0,0 +1,3 @@ +package racingcar.model.car; + +public record CarStatus(String name, int position) { } diff --git a/src/main/java/racingcar/model/car/Cars.java b/src/main/java/racingcar/model/car/Cars.java index 4fef99c89a..b6d3fd0622 100644 --- a/src/main/java/racingcar/model/car/Cars.java +++ b/src/main/java/racingcar/model/car/Cars.java @@ -56,10 +56,10 @@ public List findWinners() { .collect(Collectors.toUnmodifiableList()); } - public List getStatus() { + public List getSnapshots() { return cars.stream() - .map(Car::toString) - .collect(Collectors.toUnmodifiableList()); + .map(Car::snapshot) + .collect(Collectors.toList()); } public List getCars() { diff --git a/src/main/java/racingcar/model/game/RacingGame.java b/src/main/java/racingcar/model/game/RacingGame.java index 338e58553e..764c6f96eb 100644 --- a/src/main/java/racingcar/model/game/RacingGame.java +++ b/src/main/java/racingcar/model/game/RacingGame.java @@ -1,5 +1,6 @@ package racingcar.model.game; +import racingcar.model.car.CarStatus; import racingcar.model.car.Cars; import racingcar.model.generator.ValueGenerator; @@ -11,7 +12,7 @@ public class RacingGame { private final Cars cars; private final int attempts; - private final List> roundResults = new ArrayList<>(); + private final List> roundSnapshots = new ArrayList<>(); private RacingGame(Cars cars, int attempts) { this.cars = cars; @@ -25,16 +26,26 @@ public static RacingGame of(Cars cars, int attempts) { return new RacingGame(cars, attempts); } - public void play(ValueGenerator generator) { - for (int i = 0; i < attempts; i++) { - cars.moveAll(generator); - roundResults.add(cars.getStatus()); + public void playRound(ValueGenerator generator) { + cars.moveAll(generator); + roundSnapshots.add(cars.getSnapshots()); + } + + public List getCurrentRoundSnapshots() { + if (roundSnapshots.isEmpty()) { + return List.of(); } + List lastRound = roundSnapshots.get(roundSnapshots.size() - 1); + return lastRound.stream() + .map(status -> new CarStatus(status.name(), status.position())) + .collect(Collectors.toList()); } - public List> getRoundResults() { - return roundResults.stream() - .map(inner -> new ArrayList<>(inner)) + public List> getAllRoundSnapshots() { + return roundSnapshots.stream() + .map(round -> round.stream() + .map(status -> new CarStatus(status.name(), status.position())) + .collect(Collectors.toList())) .collect(Collectors.toList()); } diff --git a/src/main/java/racingcar/view/OutputView.java b/src/main/java/racingcar/view/OutputView.java index ea5aa15a13..b2f273c1ad 100644 --- a/src/main/java/racingcar/view/OutputView.java +++ b/src/main/java/racingcar/view/OutputView.java @@ -1,5 +1,7 @@ package racingcar.view; +import racingcar.model.car.CarStatus; + import java.util.List; public class OutputView { @@ -11,11 +13,11 @@ public void printResultHeader() { System.out.println(RESULT_HEADER); } - public void printRoundResult(List> roundResults) { - for (List round : roundResults) { - round.forEach(System.out::println); - System.out.println(); // 라운드 구분용 공백 줄 + public void printRoundResult(List snapshots) { + for (CarStatus status : snapshots) { + System.out.println(status.name() + " : " + "-".repeat(status.position())); } + System.out.println(); } public void printWinners(List winners) { From 0a609b46517e072737d4819b4132d3ce0860de1a Mon Sep 17 00:00:00 2001 From: Lee Ye Jin Date: Mon, 27 Oct 2025 19:53:28 +0900 Subject: [PATCH 19/23] =?UTF-8?q?test:=20RacingGameTest=EB=A5=BC=20DTO=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../racingcar/model/game/RacingGameTest.java | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/test/java/racingcar/model/game/RacingGameTest.java b/src/test/java/racingcar/model/game/RacingGameTest.java index 9e85a07dfc..cee5c08a92 100644 --- a/src/test/java/racingcar/model/game/RacingGameTest.java +++ b/src/test/java/racingcar/model/game/RacingGameTest.java @@ -1,6 +1,7 @@ package racingcar.model.game; import org.junit.jupiter.api.Test; +import racingcar.model.car.CarStatus; import racingcar.model.car.Cars; import java.util.List; @@ -14,12 +15,12 @@ class RacingGameTest { void 주어진_횟수만큼_경주를_진행한다() { Cars cars = Cars.valueOf(List.of("pobi", "woni")); RacingGame game = RacingGame.of(cars, 3); - - game.play(() -> 5); - List> results = game.getRoundResults(); - + for (int i = 0; i < 3; i++) { + game.playRound(() -> 5); + } + List> results = game.getAllRoundSnapshots(); assertThat(results).hasSize(3); - assertThat(results.get(2).get(0)).contains("-"); + assertThat(results.get(2).get(0).position()).isGreaterThanOrEqualTo(1); } @Test @@ -41,21 +42,23 @@ class RacingGameTest { Cars cars = Cars.valueOf(List.of("pobi")); RacingGame game = RacingGame.of(cars, 1); - game.play(() -> 9); + game.playRound(() -> 9); - List> firstCall = game.getRoundResults(); - List> secondCall = game.getRoundResults(); + List> firstCall = game.getAllRoundSnapshots(); + List> secondCall = game.getAllRoundSnapshots(); assertThat(firstCall).isNotSameAs(secondCall); - assertThat(firstCall.get(0)).isNotSameAs(secondCall.get(0)); + assertThat(firstCall.get(0).get(0)) + .usingRecursiveComparison() + .isEqualTo(secondCall.get(0).get(0)); // 값은 동일해야 함 } @Test void 최종_우승자를_반환한다() { Cars cars = Cars.valueOf(List.of("pobi", "woni")); RacingGame game = RacingGame.of(cars, 1); - game.play(() -> 9); + game.playRound(() -> 9); List winners = game.returnWinners(); assertThat(winners).containsExactlyInAnyOrder("pobi", "woni"); } From 45325f4a5b66c3b4c3025d63d47e55e93a381c42 Mon Sep 17 00:00:00 2001 From: Lee Ye Jin Date: Mon, 27 Oct 2025 20:08:00 +0900 Subject: [PATCH 20/23] =?UTF-8?q?refactor(naming):=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EB=B0=8F=20=EC=8B=9C?= =?UTF-8?q?=EA=B7=B8=EB=8B=88=EC=B2=98=20=EC=9D=BC=EA=B4=80=EC=84=B1=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../racingcar/controller/GameController.java | 7 ++++--- src/main/java/racingcar/model/car/Car.java | 5 ++--- src/main/java/racingcar/model/car/Cars.java | 16 +++++++-------- .../java/racingcar/model/game/RacingGame.java | 8 ++++---- .../java/racingcar/model/car/CarTest.java | 4 ++-- .../java/racingcar/model/car/CarsTest.java | 20 +++++++++---------- .../racingcar/model/game/RacingGameTest.java | 18 ++++++++--------- 7 files changed, 39 insertions(+), 39 deletions(-) diff --git a/src/main/java/racingcar/controller/GameController.java b/src/main/java/racingcar/controller/GameController.java index 91b951d02b..db6c2b412d 100644 --- a/src/main/java/racingcar/controller/GameController.java +++ b/src/main/java/racingcar/controller/GameController.java @@ -34,15 +34,16 @@ public void run() { String attemptsInput = inputView.readAttemptCount(); int attempts = validator.validateAttempts(attemptsInput); - Cars cars = Cars.valueOf(names); + Cars cars = Cars.of(names); RacingGame game = RacingGame.of(cars, attempts); outputView.printResultHeader(); for (int i = 0; i < attempts; i++) { game.playRound(generator); - outputView.printRoundResult(game.getCurrentRoundSnapshots()); + outputView.printRoundResult(game.currentRoundSnapshots()); } - outputView.printWinners(game.returnWinners()); + + outputView.printWinners(game.findWinners()); } } \ No newline at end of file diff --git a/src/main/java/racingcar/model/car/Car.java b/src/main/java/racingcar/model/car/Car.java index bc9cc454ea..e720feba79 100644 --- a/src/main/java/racingcar/model/car/Car.java +++ b/src/main/java/racingcar/model/car/Car.java @@ -2,7 +2,6 @@ import racingcar.model.constant.ExceptionMessages; import racingcar.model.generator.ValueGenerator; - import java.util.Objects; public class Car { @@ -37,11 +36,11 @@ private boolean isMovable(int randomValue) { return randomValue >= MOVE_THRESHOLD; } - public String getName() { + public String name() { return name; } - public int getPosition() { + public int position() { return position; } diff --git a/src/main/java/racingcar/model/car/Cars.java b/src/main/java/racingcar/model/car/Cars.java index b6d3fd0622..afc0d256e8 100644 --- a/src/main/java/racingcar/model/car/Cars.java +++ b/src/main/java/racingcar/model/car/Cars.java @@ -16,7 +16,7 @@ private Cars(List cars) { this.cars = cars; } - public static Cars valueOf(List names) { + public static Cars of(List names) { if (names == null || names.isEmpty()) { throw new IllegalArgumentException(ExceptionMessages.INPUT_EMPTY); } @@ -30,7 +30,7 @@ public static Cars valueOf(List names) { private void validateDuplicateNames(List cars) { Set uniqueNames = cars.stream() - .map(Car::getName) + .map(Car::name) .collect(Collectors.toSet()); if (uniqueNames.size() != cars.size()) { throw new IllegalArgumentException(ExceptionMessages.DUPLICATE_NAME); @@ -41,28 +41,28 @@ public void moveAll(ValueGenerator generator) { cars.forEach(car -> car.move(generator)); } - public int getMaxPosition() { + public int maxPosition() { return cars.stream() - .mapToInt(Car::getPosition) + .mapToInt(Car::position) .max() .orElse(0); } public List findWinners() { - int maxPosition = getMaxPosition(); + int maxPosition = maxPosition(); return cars.stream() .filter(car -> car.isWinner(maxPosition)) - .map(Car::getName) + .map(Car::name) .collect(Collectors.toUnmodifiableList()); } - public List getSnapshots() { + public List snapshots() { return cars.stream() .map(Car::snapshot) .collect(Collectors.toList()); } - public List getCars() { + public List cars() { return List.copyOf(cars); } } diff --git a/src/main/java/racingcar/model/game/RacingGame.java b/src/main/java/racingcar/model/game/RacingGame.java index 764c6f96eb..2fda823ab7 100644 --- a/src/main/java/racingcar/model/game/RacingGame.java +++ b/src/main/java/racingcar/model/game/RacingGame.java @@ -28,10 +28,10 @@ public static RacingGame of(Cars cars, int attempts) { public void playRound(ValueGenerator generator) { cars.moveAll(generator); - roundSnapshots.add(cars.getSnapshots()); + roundSnapshots.add(cars.snapshots()); } - public List getCurrentRoundSnapshots() { + public List currentRoundSnapshots() { if (roundSnapshots.isEmpty()) { return List.of(); } @@ -41,7 +41,7 @@ public List getCurrentRoundSnapshots() { .collect(Collectors.toList()); } - public List> getAllRoundSnapshots() { + public List> allRoundSnapshots() { return roundSnapshots.stream() .map(round -> round.stream() .map(status -> new CarStatus(status.name(), status.position())) @@ -49,7 +49,7 @@ public List> getAllRoundSnapshots() { .collect(Collectors.toList()); } - public List returnWinners() { + public List findWinners() { return cars.findWinners(); } } \ No newline at end of file diff --git a/src/test/java/racingcar/model/car/CarTest.java b/src/test/java/racingcar/model/car/CarTest.java index 5308a2e41a..05fadaad98 100644 --- a/src/test/java/racingcar/model/car/CarTest.java +++ b/src/test/java/racingcar/model/car/CarTest.java @@ -15,7 +15,7 @@ class CarTest { void 랜덤값이_4_이상이면_전진한다(int value) { Car car = new Car("pobi"); car.move(() -> value); - assertThat(car.getPosition()).isEqualTo(1); + assertThat(car.position()).isEqualTo(1); } @ParameterizedTest @@ -23,7 +23,7 @@ class CarTest { void 랜덤값이_4_미만이면_이동하지_않는다(int value) { Car car = new Car("pobi"); car.move(() -> value); - assertThat(car.getPosition()).isZero(); + assertThat(car.position()).isZero(); } @ParameterizedTest diff --git a/src/test/java/racingcar/model/car/CarsTest.java b/src/test/java/racingcar/model/car/CarsTest.java index ae34d1f073..de7802ac34 100644 --- a/src/test/java/racingcar/model/car/CarsTest.java +++ b/src/test/java/racingcar/model/car/CarsTest.java @@ -21,45 +21,45 @@ void setUp() { @Test void 중복된_이름이_있으면_예외() { List duplicateNames = List.of("pobi", "pobi"); - assertThatThrownBy(() -> Cars.valueOf(duplicateNames)) + assertThatThrownBy(() -> Cars.of(duplicateNames)) .isInstanceOf(IllegalArgumentException.class); } @Test void 빈_리스트면_예외() { - assertThatThrownBy(() -> Cars.valueOf(List.of())) + assertThatThrownBy(() -> Cars.of(List.of())) .isInstanceOf(IllegalArgumentException.class); } @Test void null_입력이면_예외() { - assertThatThrownBy(() -> Cars.valueOf(null)) + assertThatThrownBy(() -> Cars.of(null)) .isInstanceOf(IllegalArgumentException.class); } @Test void 모든_자동차가_한번씩_전진한다() { - Cars cars = Cars.valueOf(names); + Cars cars = Cars.of(names); ValueGenerator alwaysMove = () -> 9; cars.moveAll(alwaysMove); - assertThat(cars.getCars()) - .extracting(Car::getPosition) + assertThat(cars.cars()) + .extracting(Car::position) .containsExactly(1, 1, 1); } @Test void 최대_이동_거리를_반환한다() { - Cars cars = Cars.valueOf(names); + Cars cars = Cars.of(names); ValueGenerator generator = () -> 9; cars.moveAll(generator); // 모두 한 번 전진 cars.moveAll(generator); // 두 번 전진 - assertThat(cars.getMaxPosition()).isEqualTo(2); + assertThat(cars.maxPosition()).isEqualTo(2); } @Test void 공동_우승자가_존재할_수_있다() { - Cars cars = Cars.valueOf(List.of("pobi", "woni")); + Cars cars = Cars.of(List.of("pobi", "woni")); cars.moveAll(() -> 9); List winners = cars.findWinners(); assertThat(winners).containsExactlyInAnyOrder("pobi", "woni"); @@ -67,7 +67,7 @@ void setUp() { @Test void 가장_먼_위치의_자동차가_우승자이다() { - Cars cars = Cars.valueOf(names); + Cars cars = Cars.of(names); ValueGenerator moveOnlyPobi = new ValueGenerator() { private int count = 0; @Override diff --git a/src/test/java/racingcar/model/game/RacingGameTest.java b/src/test/java/racingcar/model/game/RacingGameTest.java index cee5c08a92..0f068367c3 100644 --- a/src/test/java/racingcar/model/game/RacingGameTest.java +++ b/src/test/java/racingcar/model/game/RacingGameTest.java @@ -13,39 +13,39 @@ class RacingGameTest { @Test void 주어진_횟수만큼_경주를_진행한다() { - Cars cars = Cars.valueOf(List.of("pobi", "woni")); + Cars cars = Cars.of(List.of("pobi", "woni")); RacingGame game = RacingGame.of(cars, 3); for (int i = 0; i < 3; i++) { game.playRound(() -> 5); } - List> results = game.getAllRoundSnapshots(); + List> results = game.allRoundSnapshots(); assertThat(results).hasSize(3); assertThat(results.get(2).get(0).position()).isGreaterThanOrEqualTo(1); } @Test void 시도_횟수가_0이면_예외() { - Cars cars = Cars.valueOf(List.of("pobi")); + Cars cars = Cars.of(List.of("pobi")); assertThatThrownBy(() -> RacingGame.of(cars, 0)) .isInstanceOf(IllegalArgumentException.class); } @Test void 시도_횟수가_음수면_예외() { - Cars cars = Cars.valueOf(List.of("pobi")); + Cars cars = Cars.of(List.of("pobi")); assertThatThrownBy(() -> RacingGame.of(cars, -1)) .isInstanceOf(IllegalArgumentException.class); } @Test void 라운드_결과는_깊은_복사이다() { - Cars cars = Cars.valueOf(List.of("pobi")); + Cars cars = Cars.of(List.of("pobi")); RacingGame game = RacingGame.of(cars, 1); game.playRound(() -> 9); - List> firstCall = game.getAllRoundSnapshots(); - List> secondCall = game.getAllRoundSnapshots(); + List> firstCall = game.allRoundSnapshots(); + List> secondCall = game.allRoundSnapshots(); assertThat(firstCall).isNotSameAs(secondCall); assertThat(firstCall.get(0)).isNotSameAs(secondCall.get(0)); @@ -56,10 +56,10 @@ class RacingGameTest { @Test void 최종_우승자를_반환한다() { - Cars cars = Cars.valueOf(List.of("pobi", "woni")); + Cars cars = Cars.of(List.of("pobi", "woni")); RacingGame game = RacingGame.of(cars, 1); game.playRound(() -> 9); - List winners = game.returnWinners(); + List winners = game.findWinners(); assertThat(winners).containsExactlyInAnyOrder("pobi", "woni"); } } From ac8de40ab70ef9e845ee3ee4b9a08b2a847648ff Mon Sep 17 00:00:00 2001 From: Lee Ye Jin Date: Mon, 27 Oct 2025 21:28:42 +0900 Subject: [PATCH 21/23] =?UTF-8?q?refactor=20:=20Validator=EC=9D=98=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=EC=9D=84=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=EC=9C=BC=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/racingcar/Application.java | 6 +- .../racingcar/controller/GameController.java | 27 +++----- src/main/java/racingcar/model/Validator.java | 46 ++------------ src/main/java/racingcar/model/car/Car.java | 40 ++++-------- src/main/java/racingcar/model/car/Cars.java | 4 +- .../java/racingcar/model/game/RacingGame.java | 22 ++++--- .../model/generator/RandomValueGenerator.java | 2 +- .../model/generator/ValueGenerator.java | 2 +- src/main/java/racingcar/model/vo/CarName.java | 21 +++++++ .../java/racingcar/model/vo/Position.java | 20 ++++++ .../java/racingcar/model/vo/RoundLimit.java | 34 ++++++++++ src/main/java/racingcar/view/OutputView.java | 4 ++ .../java/racingcar/model/ValidatorTest.java | 62 +++---------------- .../java/racingcar/model/car/CarsTest.java | 2 +- .../racingcar/model/game/RacingGameTest.java | 13 ++-- 15 files changed, 139 insertions(+), 166 deletions(-) create mode 100644 src/main/java/racingcar/model/vo/CarName.java create mode 100644 src/main/java/racingcar/model/vo/Position.java create mode 100644 src/main/java/racingcar/model/vo/RoundLimit.java diff --git a/src/main/java/racingcar/Application.java b/src/main/java/racingcar/Application.java index e5ac627514..ba5cc9c6ef 100644 --- a/src/main/java/racingcar/Application.java +++ b/src/main/java/racingcar/Application.java @@ -1,7 +1,6 @@ package racingcar; import racingcar.controller.GameController; -import racingcar.model.Validator; import racingcar.model.generator.RandomValueGenerator; import racingcar.model.generator.ValueGenerator; import racingcar.view.InputView; @@ -11,10 +10,9 @@ public class Application { public static void main(String[] args) { InputView inputView = new InputView(); OutputView outputView = new OutputView(); - Validator validator = new Validator(); ValueGenerator generator = new RandomValueGenerator(); - GameController controller = new GameController(inputView, outputView, validator, generator); + GameController controller = new GameController(inputView, outputView, generator); controller.run(); } -} \ No newline at end of file +} diff --git a/src/main/java/racingcar/controller/GameController.java b/src/main/java/racingcar/controller/GameController.java index db6c2b412d..a2b70a9dcc 100644 --- a/src/main/java/racingcar/controller/GameController.java +++ b/src/main/java/racingcar/controller/GameController.java @@ -3,47 +3,36 @@ import racingcar.model.car.Cars; import racingcar.model.game.RacingGame; import racingcar.model.generator.ValueGenerator; -import racingcar.model.Validator; +import racingcar.model.vo.RoundLimit; import racingcar.view.InputView; import racingcar.view.OutputView; import java.util.List; public class GameController { + private final InputView inputView; private final OutputView outputView; - private final Validator validator; private final ValueGenerator generator; - public GameController( - InputView inputView, - OutputView outputView, - Validator validator, - ValueGenerator generator - ) { + public GameController(InputView inputView, OutputView outputView, ValueGenerator generator) { this.inputView = inputView; this.outputView = outputView; - this.validator = validator; this.generator = generator; } public void run() { List names = inputView.readCarNames(); - validator.validateNames(names); - - String attemptsInput = inputView.readAttemptCount(); - int attempts = validator.validateAttempts(attemptsInput); + String attemptInput = inputView.readAttemptCount(); Cars cars = Cars.of(names); - RacingGame game = RacingGame.of(cars, attempts); - + RoundLimit roundLimit = RoundLimit.of(attemptInput); + RacingGame game = RacingGame.of(cars, roundLimit); outputView.printResultHeader(); - - for (int i = 0; i < attempts; i++) { + for (int i = 0; i < roundLimit.value(); i++) { game.playRound(generator); outputView.printRoundResult(game.currentRoundSnapshots()); } - outputView.printWinners(game.findWinners()); } -} \ No newline at end of file +} diff --git a/src/main/java/racingcar/model/Validator.java b/src/main/java/racingcar/model/Validator.java index 1a06e15602..6a0bdc8114 100644 --- a/src/main/java/racingcar/model/Validator.java +++ b/src/main/java/racingcar/model/Validator.java @@ -1,51 +1,17 @@ package racingcar.model; import racingcar.model.constant.ExceptionMessages; -import java.util.HashSet; -import java.util.List; -import java.util.Set; public class Validator { - public Validator() { - } - - public void validateNames(List names) { - if (names == null || names.isEmpty()) { - throw new IllegalArgumentException(ExceptionMessages.INPUT_EMPTY); - } - - for (String name : names) { - if (name == null || name.isBlank()) { - throw new IllegalArgumentException(ExceptionMessages.INPUT_EMPTY); - } - if (name.length() > 5) { - throw new IllegalArgumentException(ExceptionMessages.INVALID_NAME_LENGTH); - } - } - - Set unique = new HashSet<>(names); - if (unique.size() != names.size()) { - throw new IllegalArgumentException(ExceptionMessages.DUPLICATE_NAME); - } - } - - public int validateAttempts(String input) { - if (input == null || input.isBlank()) { - throw new IllegalArgumentException(ExceptionMessages.INPUT_EMPTY); - } + public Validator() {} - int count; - try { - count = Integer.parseInt(input); - } catch (NumberFormatException e) { - throw new IllegalArgumentException(ExceptionMessages.INVALID_NUMBER_FORMAT); + public void validateInputExists(String input) { + if (input == null) { + throw new IllegalArgumentException(ExceptionMessages.INPUT_NULL.get()); } - - if (count <= 0) { - throw new IllegalArgumentException(ExceptionMessages.INVALID_NUMBER_RANGE); + if (input.isBlank()) { + throw new IllegalArgumentException(ExceptionMessages.INPUT_BLANK.get()); } - - return count; } } diff --git a/src/main/java/racingcar/model/car/Car.java b/src/main/java/racingcar/model/car/Car.java index e720feba79..ce75e91fbf 100644 --- a/src/main/java/racingcar/model/car/Car.java +++ b/src/main/java/racingcar/model/car/Car.java @@ -1,54 +1,36 @@ package racingcar.model.car; -import racingcar.model.constant.ExceptionMessages; import racingcar.model.generator.ValueGenerator; -import java.util.Objects; +import racingcar.model.vo.CarName; +import racingcar.model.vo.Position; public class Car { - private static final int MOVE_THRESHOLD = 4; - private static final int INITIAL_POSITION = 0; - - private final String name; - private int position = INITIAL_POSITION; + private final CarName name; + private final Position position; public Car(String name) { - validateName(name); - this.name = name; - } - - private void validateName(String name) { - if (Objects.isNull(name) || name.isBlank()) { - throw new IllegalArgumentException(ExceptionMessages.INVALID_NAME_EMPTY); - } - if (name.length() > 5) { - throw new IllegalArgumentException(ExceptionMessages.INVALID_NAME_LENGTH); - } + this.name = new CarName(name); + this.position = new Position(); } public void move(ValueGenerator generator) { - if (isMovable(generator.getValue())) { - position++; - } - } - - private boolean isMovable(int randomValue) { - return randomValue >= MOVE_THRESHOLD; + position.move(generator.getValue()); } public String name() { - return name; + return name.value(); } public int position() { - return position; + return position.value(); } public boolean isWinner(int maxPosition) { - return this.position == maxPosition; + return position.value() == maxPosition; } public CarStatus snapshot() { - return new CarStatus(name, position); + return new CarStatus(name.value(), position.value()); } } diff --git a/src/main/java/racingcar/model/car/Cars.java b/src/main/java/racingcar/model/car/Cars.java index afc0d256e8..245e2d2d61 100644 --- a/src/main/java/racingcar/model/car/Cars.java +++ b/src/main/java/racingcar/model/car/Cars.java @@ -18,7 +18,7 @@ private Cars(List cars) { public static Cars of(List names) { if (names == null || names.isEmpty()) { - throw new IllegalArgumentException(ExceptionMessages.INPUT_EMPTY); + throw new IllegalArgumentException("자동차 이름 목록이 비어 있습니다."); } List carList = names.stream() @@ -33,7 +33,7 @@ private void validateDuplicateNames(List cars) { .map(Car::name) .collect(Collectors.toSet()); if (uniqueNames.size() != cars.size()) { - throw new IllegalArgumentException(ExceptionMessages.DUPLICATE_NAME); + throw new IllegalArgumentException(ExceptionMessages.DUPLICATE_NAME.get()); } } diff --git a/src/main/java/racingcar/model/game/RacingGame.java b/src/main/java/racingcar/model/game/RacingGame.java index 2fda823ab7..baf0af74d6 100644 --- a/src/main/java/racingcar/model/game/RacingGame.java +++ b/src/main/java/racingcar/model/game/RacingGame.java @@ -2,7 +2,9 @@ import racingcar.model.car.CarStatus; import racingcar.model.car.Cars; +import racingcar.model.constant.ExceptionMessages; import racingcar.model.generator.ValueGenerator; +import racingcar.model.vo.RoundLimit; import java.util.ArrayList; import java.util.List; @@ -11,24 +13,26 @@ public class RacingGame { private final Cars cars; - private final int attempts; + private final RoundLimit roundLimit; private final List> roundSnapshots = new ArrayList<>(); + private int currentRound = 0; - private RacingGame(Cars cars, int attempts) { + private RacingGame(Cars cars, RoundLimit roundLimit) { this.cars = cars; - this.attempts = attempts; + this.roundLimit = roundLimit; } - public static RacingGame of(Cars cars, int attempts) { - if (attempts <= 0) { - throw new IllegalArgumentException("시도 횟수는 1 이상이어야 합니다."); - } - return new RacingGame(cars, attempts); + public static RacingGame of(Cars cars, RoundLimit roundLimit) { + return new RacingGame(cars, roundLimit); } public void playRound(ValueGenerator generator) { + if (!roundLimit.hasRemaining(currentRound)) { + throw new IllegalStateException(ExceptionMessages.EXCEEDED_ROUND_LIMIT.get()); + } cars.moveAll(generator); roundSnapshots.add(cars.snapshots()); + currentRound++; } public List currentRoundSnapshots() { @@ -52,4 +56,4 @@ public List> allRoundSnapshots() { public List findWinners() { return cars.findWinners(); } -} \ No newline at end of file +} diff --git a/src/main/java/racingcar/model/generator/RandomValueGenerator.java b/src/main/java/racingcar/model/generator/RandomValueGenerator.java index 2cfdeba532..acc337589f 100644 --- a/src/main/java/racingcar/model/generator/RandomValueGenerator.java +++ b/src/main/java/racingcar/model/generator/RandomValueGenerator.java @@ -11,4 +11,4 @@ public class RandomValueGenerator implements ValueGenerator { public int getValue() { return Randoms.pickNumberInRange(MIN, MAX); } -} \ No newline at end of file +} diff --git a/src/main/java/racingcar/model/generator/ValueGenerator.java b/src/main/java/racingcar/model/generator/ValueGenerator.java index 4a7b6f2c3f..1a79d8ca22 100644 --- a/src/main/java/racingcar/model/generator/ValueGenerator.java +++ b/src/main/java/racingcar/model/generator/ValueGenerator.java @@ -3,4 +3,4 @@ @FunctionalInterface public interface ValueGenerator { int getValue(); -} \ No newline at end of file +} diff --git a/src/main/java/racingcar/model/vo/CarName.java b/src/main/java/racingcar/model/vo/CarName.java new file mode 100644 index 0000000000..e81b495bc8 --- /dev/null +++ b/src/main/java/racingcar/model/vo/CarName.java @@ -0,0 +1,21 @@ +package racingcar.model.vo; + +import racingcar.model.constant.ExceptionMessages; + +public record CarName(String value) { + private static final int NAME_MAX_LENGTH = 5; + + public CarName { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException(ExceptionMessages.INVALID_NAME_EMPTY.get()); + } + if (value.length() > NAME_MAX_LENGTH) { + throw new IllegalArgumentException(ExceptionMessages.INVALID_NAME_LENGTH.get()); + } + } + + @Override + public String toString() { + return value; + } +} diff --git a/src/main/java/racingcar/model/vo/Position.java b/src/main/java/racingcar/model/vo/Position.java new file mode 100644 index 0000000000..423e1a5232 --- /dev/null +++ b/src/main/java/racingcar/model/vo/Position.java @@ -0,0 +1,20 @@ +package racingcar.model.vo; + +public class Position { + private int value; + private static final int MOVE_THRESHOLD = 4; + + public Position() { + this.value = 0; + } + + public int value() { + return value; + } + + public void move(int randomValue) { + if (randomValue >= MOVE_THRESHOLD) { + value++; + } + } +} diff --git a/src/main/java/racingcar/model/vo/RoundLimit.java b/src/main/java/racingcar/model/vo/RoundLimit.java new file mode 100644 index 0000000000..c3bf1425d1 --- /dev/null +++ b/src/main/java/racingcar/model/vo/RoundLimit.java @@ -0,0 +1,34 @@ +package racingcar.model.vo; + +import racingcar.model.constant.ExceptionMessages; + +public final class RoundLimit { + + private final int value; + + private RoundLimit(int value) { + if (value <= 0) { + throw new IllegalArgumentException(ExceptionMessages.INVALID_NUMBER_RANGE.get()); + } + this.value = value; + } + + public static RoundLimit of(String input) { + if (input == null || input.isBlank()) { + throw new IllegalArgumentException(ExceptionMessages.INPUT_EMPTY.get()); + } + try { + return new RoundLimit(Integer.parseInt(input)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(ExceptionMessages.INVALID_NUMBER_FORMAT.get()); + } + } + + public boolean hasRemaining(int currentRound) { + return currentRound < value; + } + + public int value() { + return value; + } +} diff --git a/src/main/java/racingcar/view/OutputView.java b/src/main/java/racingcar/view/OutputView.java index b2f273c1ad..fe6c95e3cc 100644 --- a/src/main/java/racingcar/view/OutputView.java +++ b/src/main/java/racingcar/view/OutputView.java @@ -23,4 +23,8 @@ public void printRoundResult(List snapshots) { public void printWinners(List winners) { System.out.println(WINNER_ANNOUNCEMENT + String.join(", ", winners)); } + + public void printErrorMessage(String message) { + System.out.println("[ERROR] " + message); + } } diff --git a/src/test/java/racingcar/model/ValidatorTest.java b/src/test/java/racingcar/model/ValidatorTest.java index 8c17a9d113..1a444cb6a2 100644 --- a/src/test/java/racingcar/model/ValidatorTest.java +++ b/src/test/java/racingcar/model/ValidatorTest.java @@ -3,72 +3,28 @@ import org.junit.jupiter.api.Test; import racingcar.model.constant.ExceptionMessages; -import java.util.List; - import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.Assertions.assertThat; class ValidatorTest { private final Validator validator = new Validator(); @Test - void 이름이_null이거나_빈문자열이면_예외() { - assertThatThrownBy(() -> validator.validateNames(null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage(ExceptionMessages.INPUT_EMPTY); - - assertThatThrownBy(() -> validator.validateNames(List.of())) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage(ExceptionMessages.INPUT_EMPTY); - } - - @Test - void 이름이_공백이거나_5자초과이면_예외() { - assertThatThrownBy(() -> validator.validateNames(List.of(" "))) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage(ExceptionMessages.INPUT_EMPTY); - - assertThatThrownBy(() -> validator.validateNames(List.of("abcdef"))) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage(ExceptionMessages.INVALID_NAME_LENGTH); - } - - @Test - void 이름이_중복이면_예외() { - assertThatThrownBy(() -> validator.validateNames(List.of("pobi", "pobi"))) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage(ExceptionMessages.DUPLICATE_NAME); - } - - @Test - void 시도횟수가_null이거나_공백이면_예외() { - assertThatThrownBy(() -> validator.validateAttempts(null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage(ExceptionMessages.INPUT_EMPTY); - - assertThatThrownBy(() -> validator.validateAttempts(" ")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage(ExceptionMessages.INPUT_EMPTY); - } - - @Test - void 시도횟수가_숫자가_아니면_예외() { - assertThatThrownBy(() -> validator.validateAttempts("abc")) + void 입력값이_null이면_예외() { + assertThatThrownBy(() -> validator.validateInputExists(null)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage(ExceptionMessages.INVALID_NUMBER_FORMAT); + .hasMessage(ExceptionMessages.INPUT_NULL.get()); } @Test - void 시도횟수가_0이하면_예외() { - assertThatThrownBy(() -> validator.validateAttempts("0")) + void 입력값이_공백이면_예외() { + assertThatThrownBy(() -> validator.validateInputExists(" ")) .isInstanceOf(IllegalArgumentException.class) - .hasMessage(ExceptionMessages.INVALID_NUMBER_RANGE); + .hasMessage(ExceptionMessages.INPUT_BLANK.get()); } @Test - void 시도횟수가_유효하면_정수로_반환한다() { - int count = validator.validateAttempts("3"); - assertThat(count).isEqualTo(3); + void 입력값이_정상이라면_예외가_발생하지_않는다() { + validator.validateInputExists("pobi"); // 예외 발생하지 않아야 함 } -} \ No newline at end of file +} diff --git a/src/test/java/racingcar/model/car/CarsTest.java b/src/test/java/racingcar/model/car/CarsTest.java index de7802ac34..ee2821f5a7 100644 --- a/src/test/java/racingcar/model/car/CarsTest.java +++ b/src/test/java/racingcar/model/car/CarsTest.java @@ -80,4 +80,4 @@ public int getValue() { assertThat(winners) .containsExactly("pobi"); } -} \ No newline at end of file +} diff --git a/src/test/java/racingcar/model/game/RacingGameTest.java b/src/test/java/racingcar/model/game/RacingGameTest.java index 0f068367c3..eae069d3ba 100644 --- a/src/test/java/racingcar/model/game/RacingGameTest.java +++ b/src/test/java/racingcar/model/game/RacingGameTest.java @@ -3,6 +3,7 @@ import org.junit.jupiter.api.Test; import racingcar.model.car.CarStatus; import racingcar.model.car.Cars; +import racingcar.model.vo.RoundLimit; import java.util.List; @@ -14,7 +15,7 @@ class RacingGameTest { @Test void 주어진_횟수만큼_경주를_진행한다() { Cars cars = Cars.of(List.of("pobi", "woni")); - RacingGame game = RacingGame.of(cars, 3); + RacingGame game = RacingGame.of(cars, RoundLimit.of("3")); for (int i = 0; i < 3; i++) { game.playRound(() -> 5); } @@ -25,22 +26,20 @@ class RacingGameTest { @Test void 시도_횟수가_0이면_예외() { - Cars cars = Cars.of(List.of("pobi")); - assertThatThrownBy(() -> RacingGame.of(cars, 0)) + assertThatThrownBy(() -> RoundLimit.of("0")) .isInstanceOf(IllegalArgumentException.class); } @Test void 시도_횟수가_음수면_예외() { - Cars cars = Cars.of(List.of("pobi")); - assertThatThrownBy(() -> RacingGame.of(cars, -1)) + assertThatThrownBy(() -> RoundLimit.of("-1")) .isInstanceOf(IllegalArgumentException.class); } @Test void 라운드_결과는_깊은_복사이다() { Cars cars = Cars.of(List.of("pobi")); - RacingGame game = RacingGame.of(cars, 1); + RacingGame game = RacingGame.of(cars, RoundLimit.of("1")); game.playRound(() -> 9); @@ -57,7 +56,7 @@ class RacingGameTest { @Test void 최종_우승자를_반환한다() { Cars cars = Cars.of(List.of("pobi", "woni")); - RacingGame game = RacingGame.of(cars, 1); + RacingGame game = RacingGame.of(cars, RoundLimit.of("1")); game.playRound(() -> 9); List winners = game.findWinners(); assertThat(winners).containsExactlyInAnyOrder("pobi", "woni"); From 90e7c65dcffd60d606785fb040336b46d177e8ac Mon Sep 17 00:00:00 2001 From: Lee Ye Jin Date: Mon, 27 Oct 2025 21:29:16 +0900 Subject: [PATCH 22/23] =?UTF-8?q?refactor=20:=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=83=81=EC=88=98=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/constant/ExceptionMessages.java | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/main/java/racingcar/model/constant/ExceptionMessages.java b/src/main/java/racingcar/model/constant/ExceptionMessages.java index 8144b07a7f..8603dcb28a 100644 --- a/src/main/java/racingcar/model/constant/ExceptionMessages.java +++ b/src/main/java/racingcar/model/constant/ExceptionMessages.java @@ -1,20 +1,26 @@ package racingcar.model.constant; -public final class ExceptionMessages { +public enum ExceptionMessages { - private ExceptionMessages() {} + INPUT_EMPTY("입력값이 비어있습니다."), + INPUT_NULL("입력값이 존재하지 않습니다."), + INPUT_BLANK("입력값에 공백만 포함될 수 없습니다."), - // 공통 입력 예외 - public static final String INPUT_EMPTY = "입력값이 비어있습니다."; - public static final String INPUT_NULL = "입력값이 존재하지 않습니다."; - public static final String INPUT_BLANK = "입력값에 공백만 포함될 수 없습니다."; + INVALID_NAME_EMPTY("자동차 이름은 비어있을 수 없습니다."), + INVALID_NAME_LENGTH("자동차 이름은 1자 이상 5자 이하만 가능합니다."), + DUPLICATE_NAME("자동차 이름은 중복될 수 없습니다."), - // 자동차 이름 예외 - public static final String INVALID_NAME_EMPTY = "자동차 이름은 비어있을 수 없습니다."; - public static final String INVALID_NAME_LENGTH = "자동차 이름은 1자 이상 5자 이하만 가능합니다."; - public static final String DUPLICATE_NAME = "자동차 이름은 중복될 수 없습니다."; + INVALID_NUMBER_FORMAT("시도 횟수는 숫자여야 합니다."), + INVALID_NUMBER_RANGE("시도 횟수는 1 이상의 정수여야 합니다."), + EXCEEDED_ROUND_LIMIT("라운드 한도를 초과했습니다."); - // 시도 횟수 관련 예외 - public static final String INVALID_NUMBER_FORMAT = "시도 횟수는 숫자여야 합니다."; - public static final String INVALID_NUMBER_RANGE = "시도 횟수는 1 이상의 정수여야 합니다."; + private final String message; + + ExceptionMessages(String message) { + this.message = message; + } + + public String get() { + return message; + } } From 4575bfb3795adf170c5a1730d3d6d67a1ee40bfc Mon Sep 17 00:00:00 2001 From: Lee Ye Jin Date: Mon, 27 Oct 2025 21:44:23 +0900 Subject: [PATCH 23/23] =?UTF-8?q?style:=20=EC=BD=94=EB=93=9C=20=EC=BB=A8?= =?UTF-8?q?=EB=B2=A4=EC=85=98=20=EC=A4=80=EC=88=98=20=EB=B0=8F=20=EC=8A=A4?= =?UTF-8?q?=ED=83=80=EC=9D=BC=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/racingcar/model/Validator.java | 2 -- src/main/java/racingcar/model/car/Cars.java | 2 +- src/main/java/racingcar/model/game/RacingGame.java | 4 +--- src/main/java/racingcar/view/InputView.java | 3 --- src/main/java/racingcar/view/OutputView.java | 4 ---- 5 files changed, 2 insertions(+), 13 deletions(-) diff --git a/src/main/java/racingcar/model/Validator.java b/src/main/java/racingcar/model/Validator.java index 6a0bdc8114..2b6c565f15 100644 --- a/src/main/java/racingcar/model/Validator.java +++ b/src/main/java/racingcar/model/Validator.java @@ -4,8 +4,6 @@ public class Validator { - public Validator() {} - public void validateInputExists(String input) { if (input == null) { throw new IllegalArgumentException(ExceptionMessages.INPUT_NULL.get()); diff --git a/src/main/java/racingcar/model/car/Cars.java b/src/main/java/racingcar/model/car/Cars.java index 245e2d2d61..4e4a96d7fb 100644 --- a/src/main/java/racingcar/model/car/Cars.java +++ b/src/main/java/racingcar/model/car/Cars.java @@ -18,7 +18,7 @@ private Cars(List cars) { public static Cars of(List names) { if (names == null || names.isEmpty()) { - throw new IllegalArgumentException("자동차 이름 목록이 비어 있습니다."); + throw new IllegalArgumentException(ExceptionMessages.INPUT_EMPTY.get()); } List carList = names.stream() diff --git a/src/main/java/racingcar/model/game/RacingGame.java b/src/main/java/racingcar/model/game/RacingGame.java index baf0af74d6..210ee774d7 100644 --- a/src/main/java/racingcar/model/game/RacingGame.java +++ b/src/main/java/racingcar/model/game/RacingGame.java @@ -15,7 +15,6 @@ public class RacingGame { private final Cars cars; private final RoundLimit roundLimit; private final List> roundSnapshots = new ArrayList<>(); - private int currentRound = 0; private RacingGame(Cars cars, RoundLimit roundLimit) { this.cars = cars; @@ -27,12 +26,11 @@ public static RacingGame of(Cars cars, RoundLimit roundLimit) { } public void playRound(ValueGenerator generator) { - if (!roundLimit.hasRemaining(currentRound)) { + if (!roundLimit.hasRemaining(roundSnapshots.size())) { throw new IllegalStateException(ExceptionMessages.EXCEEDED_ROUND_LIMIT.get()); } cars.moveAll(generator); roundSnapshots.add(cars.snapshots()); - currentRound++; } public List currentRoundSnapshots() { diff --git a/src/main/java/racingcar/view/InputView.java b/src/main/java/racingcar/view/InputView.java index 333c117c72..28827d30af 100644 --- a/src/main/java/racingcar/view/InputView.java +++ b/src/main/java/racingcar/view/InputView.java @@ -9,9 +9,6 @@ public class InputView { private static final String INPUT_CAR_NAMES_MESSAGE = "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"; private static final String INPUT_ATTEMPTS_MESSAGE = "시도할 횟수는 몇 회인가요?"; - public InputView() { - } - public List readCarNames() { System.out.println(INPUT_CAR_NAMES_MESSAGE); String input = Console.readLine(); diff --git a/src/main/java/racingcar/view/OutputView.java b/src/main/java/racingcar/view/OutputView.java index fe6c95e3cc..b2f273c1ad 100644 --- a/src/main/java/racingcar/view/OutputView.java +++ b/src/main/java/racingcar/view/OutputView.java @@ -23,8 +23,4 @@ public void printRoundResult(List snapshots) { public void printWinners(List winners) { System.out.println(WINNER_ANNOUNCEMENT + String.join(", ", winners)); } - - public void printErrorMessage(String message) { - System.out.println("[ERROR] " + message); - } }