Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a9e99fb
docs(README): 기능 목록과 구조 설계 초안 추가
Hanbeeen Oct 27, 2025
a0921d2
test(domain): Car 이동 규칙 테스트 추가
Hanbeeen Oct 27, 2025
ad54cbb
feat(domain): Car 도메인 및 이동 로직 구현
Hanbeeen Oct 27, 2025
610aa19
refactor(domain): CarTest 메서드 이름 및 디렉토리 구조 변경
Hanbeeen Oct 27, 2025
c26688b
test(validator): 자동차 이름 검증 테스트 추가
Hanbeeen Oct 27, 2025
166f0aa
feat(validator): 자동차 이름 유효성 검증 로직 구현
Hanbeeen Oct 27, 2025
4e39dc5
test(validator): 시도 횟수 검증 테스트 추가
Hanbeeen Oct 27, 2025
29827d1
feat(validator): 시도 횟수 유효성 검증 로직 구현
Hanbeeen Oct 27, 2025
f8396cb
refactor(validator): 입력값 trim 처리로 유효성 검사 로직 보강
Hanbeeen Oct 27, 2025
65b9357
test(validator): 0 이하 입력 예외 검증을 파라미터화
Hanbeeen Oct 27, 2025
b3eb87c
test(domain): Cars 전진 처리 및 읽기 전용 조회 테스트 추가
Hanbeeen Oct 27, 2025
2280820
feat(domain): Cars 전진 처리 및 읽기 전용 조회 추가
Hanbeeen Oct 27, 2025
49e397c
test(service): RacingGameService 우승자 계산(getWinners) 테스트 추가
Hanbeeen Oct 27, 2025
68b388b
feat(service): RacingGameService 라운드 진행 및 우승자 계산 로직 추가
Hanbeeen Oct 27, 2025
a6f317e
test(view): InputView 파싱 및 입력 유효성 검증 테스트 추가
Hanbeeen Oct 27, 2025
e53c838
feat(view): InputView 자동차 이름 및 시도 횟수 입력 및 파싱 로직 추가
Hanbeeen Oct 27, 2025
9bc06f4
test(view): OutputView 포맷팅 로직 테스트 추가
Hanbeeen Oct 27, 2025
508082b
feat(view): OutputView 출력 및 포맷팅 로직 추가
Hanbeeen Oct 27, 2025
e4e6531
feat(controller): GameController 추가
Hanbeeen Oct 27, 2025
69ee09d
feat(controller): Application.main 추가
Hanbeeen Oct 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,117 @@
# java-racingcar-precourse

## 기능 목록

### 1. 입력
1.1 경주할 자동차 이름 입력
- "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)" 문구 출력 후 문자열 입력
- 예: `pobi,woni,jun`
- 쉼표(,) 기준 분리
- 각 이름 앞뒤 공백 제거

1.2 이동 횟수 입력
- "시도할 횟수는 몇 회인가요?" 문구 출력 후 문자열 입력
- 양의 정수 기대


### 2. 입력값 검증
잘못된 입력값일 경우 `IllegalArgumentException` 발생 후 프로그램 종료

2.1 자동차 이름 검증
- 빈 이름 금지
- 공백만 있는 이름 금지
- 이름 길이 5자 이하

2.2 이동 횟수 검증
- 정수 변환 불가능한 입력값 예외 처리
- 1 미만 값 예외 처리


### 3. 경주 로직

3.1 자동차 초기화
- 검증 통과한 이름 목록으로 자동차 객체 생성
- 각 자동차는 이름과 현재 위치(position) 보유
- 초기 position 값 0

3.2 단일 라운드 진행
- 각 자동차마다 0부터 9 사이 무작위 정수 생성
- 생성된 값이 4 이상이면 position 1 증가
- 4 미만이면 이동 없음

3.3 전체 레이스
- 입력된 시도 횟수만큼 라운드 반복
- 매 라운드 종료 시 현재 상태를 출력 대상으로 준비


### 4. 출력

4.1 라운드별 현황 출력
- 한 라운드마다 모든 자동차 상태 출력
- 형식: `이름 : 현재 position 수만큼 '-'`
- 예: `pobi : ---`
- 라운드마다 전체 출력 후 빈 줄 출력

4.2 최종 우승자 출력
- 모든 라운드 종료 후 최대로 전진한 자동차를 우승자로 판단
- 공동 우승 가능
- 여러 우승자일 경우 쉼표(,) 연결
- 예: `최종 우승자 : pobi, jun`
- 우승자 한 명일 경우 단일 이름만 출력


## 구조 계획
OOP, MVC, TDD 개념을 최대한 고려하여 설계

### Application
- `Application.main()`에서 프로그램 시작
- 컨트롤러를 한 번 호출해서 전체 게임 진행

### GameController
- 전체 흐름 조립 책임
- 입력 요청 → 검증 → 라운드 반복 진행 → 라운드별 결과 출력 → 최종 우승자 출력
- 비즈니스 로직(경주 규칙)과 입출력 로직을 연결하는 계층 역할

### InputView / OutputView
- InputView
- 안내 문구 출력 후 `Console.readLine()`으로 입력 수집
- 가공하지 않은 문자열 반환
- OutputView
- 라운드별 현황 출력
- 최종 우승자 출력
- 출력 형식 고정
- View 계층은 경주 로직을 모르게 유지. 출력만 수행

### Validator
- 자동차 이름 유효성 검증
- 빈 문자열 금지
- 공백만 있는 문자열 금지
- 5자 초과 금지
- 시도 횟수 유효성 검증
- 정수 여부
- 1 이상 여부
- 위반 시 `IllegalArgumentException` 발생
- 입력(InputView)과 검증(Validator)을 분리해 단일 책임 유지(SRP)

### Domain / Service
- Car
- 자동차 이름과 현재 위치 필드
- 이동 가능 여부를 받아 position 증가 여부 결정
- Cars
- 자동차 여러 대를 관리
- 라운드 단위 이동 처리 대상
- RacingGameService
- 한 라운드 진행
- 각 자동차마다 난수 생성 후 이동 여부 결정
- 최종 우승자 계산
- 최댓값 위치를 기준으로 우승 후보 목록 생성
- Service와 Domain은 View에 의존하지 않도록 설계
- JUnit5, AssertJ 단위 테스트 작성 대상

### 테스트 계획
- Car 이동 규칙 테스트
- 임의의 숫자 주입 후 position 증가 여부 확인
- 우승자 계산 테스트
- 여러 자동차 중 최댓값 위치를 가진 자동차 목록 계산 결과 확인
- 검증 로직 테스트
- 잘못된 이름/잘못된 횟수 입력 시 `IllegalArgumentException` 처리 여부 확인
7 changes: 5 additions & 2 deletions src/main/java/racingcar/Application.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package racingcar;

import racingcar.controller.GameController;

public class Application {
public static void main(String[] args) {
// TODO: 프로그램 구현
GameController controller = new GameController();
controller.run();
}
}
}
54 changes: 54 additions & 0 deletions src/main/java/racingcar/controller/GameController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package racingcar.controller;

import racingcar.domain.Car;
import racingcar.domain.Cars;
import racingcar.service.RacingGameService;
import racingcar.view.InputView;
import racingcar.view.OutputView;

import java.util.ArrayList;
import java.util.List;

public class GameController {

private final InputView inputView;
private final OutputView outputView;
private final RacingGameService racingGameService;

public GameController() {
this.inputView = new InputView();
this.outputView = new OutputView();
this.racingGameService = new RacingGameService();
}

public void run() {
// 1. 입력
List<String> carNames = inputView.inputCarNames();
int attemptCount = inputView.inputAttemptCount();

// 2. Cars 생성
Cars cars = createCars(carNames);

// 3. 실행 결과 헤더 출력
System.out.println();
System.out.println("실행 결과");

// 4. 시도 횟수만큼 라운드 진행
for (int i = 0; i < attemptCount; i++) {
racingGameService.proceedRound(cars);
outputView.printRoundResult(cars);
}

// 5. 최종 우승자 출력
List<String> winners = racingGameService.getWinners(cars);
outputView.printWinners(winners);
}

private Cars createCars(List<String> names) {
List<Car> list = new ArrayList<>();
for (String name : names) {
list.add(new Car(name));
}
return new Cars(list);
}
}
27 changes: 27 additions & 0 deletions src/main/java/racingcar/domain/Car.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package racingcar.domain;

public class Car {
private static final int MOVE_THRESHOLD = 4;

private final String name;
private int position;

public Car(String name) {
this.name = name;
this.position = 0;
}

public void moveIfPossible(int number) {
if (number >= MOVE_THRESHOLD) {
position++;
}
}

public int getPosition() {
return position;
}

public String getName() {
return name;
}
}
27 changes: 27 additions & 0 deletions src/main/java/racingcar/domain/Cars.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package racingcar.domain;

import java.util.Collections;
import java.util.List;

public class Cars {

private final List<Car> cars;

public Cars(List<Car> cars) {
this.cars = cars;
}

// 한 라운드에서 각 자동차가 받을 난수 리스트를 받아서 이동 시도
public void moveAllWith(List<Integer> numbers) {
for (int i = 0; i < cars.size(); i++) {
Car car = cars.get(i);
int number = numbers.get(i);
car.moveIfPossible(number);
}
}

// 현재 상태를 조회할 수 있도록 읽기 전용 조회 제공
public List<Car> asList() {
return Collections.unmodifiableList(cars);
}
}
38 changes: 38 additions & 0 deletions src/main/java/racingcar/service/RacingGameService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package racingcar.service;

import camp.nextstep.edu.missionutils.Randoms;
import racingcar.domain.Car;
import racingcar.domain.Cars;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class RacingGameService {
// 라운드 1회 실행
public void proceedRound(Cars cars) {
List<Integer> numbers = new ArrayList<>();

for (int i = 0; i < cars.asList().size(); i++) {
int randomNumber = Randoms.pickNumberInRange(0, 9);
numbers.add(randomNumber);
}

cars.moveAllWith(numbers);
}

// 우승자 계산
public List<String> getWinners(Cars cars) {
// 이동거리 최대값 찾기
int max = cars.asList().stream()
.mapToInt(Car::getPosition)
.max()
.orElse(0);

// 이동거리 최대값을 가지는 자동차 모두 반환
return cars.asList().stream()
.filter(car -> car.getPosition() == max)
.map(Car::getName)
.collect(Collectors.toList());
}
}
23 changes: 23 additions & 0 deletions src/main/java/racingcar/validator/AttemptCountValidator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package racingcar.validator;

public class AttemptCountValidator {

private AttemptCountValidator() {
}

public static void validate(String rawCount) {
int count = parseInt(rawCount);

if (count < 1) {
throw new IllegalArgumentException("시도 횟수는 1 이상의 정수여야 합니다.");
}
}

private static int parseInt(String rawCount) {
try {
return Integer.parseInt(rawCount.trim());
} catch (NumberFormatException e) {
throw new IllegalArgumentException("시도 횟수는 숫자여야 합니다.");
}
}
}
25 changes: 25 additions & 0 deletions src/main/java/racingcar/validator/CarNameValidator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package racingcar.validator;

public class CarNameValidator {

private static final int MAX_NAME_LENGTH = 5;

private CarNameValidator() {
}

public static void validate(String rawName) {
if (rawName == null) {
throw new IllegalArgumentException("자동차 이름은 null일 수 없습니다.");
}

String name = rawName.replaceAll(" ", "");

if (name.isEmpty()) {
throw new IllegalArgumentException("자동차 이름이 비어 있을 수 없습니다.");
}

if (name.length() > MAX_NAME_LENGTH) {
throw new IllegalArgumentException("자동차 이름은 5자 이하여야 합니다.");
}
}
}
43 changes: 43 additions & 0 deletions src/main/java/racingcar/view/InputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package racingcar.view;

import camp.nextstep.edu.missionutils.Console;
import racingcar.validator.CarNameValidator;
import racingcar.validator.AttemptCountValidator;

import java.util.ArrayList;
import java.util.List;

public class InputView {

public List<String> inputCarNames() {
System.out.println("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)");
String rawCarNames = Console.readLine();
return parseCarNames(rawCarNames);
}

public int inputAttemptCount() {
System.out.println("시도할 횟수는 몇 회인가요?");
String rawAttemptCount = Console.readLine();
return parseAttemptCount(rawAttemptCount);
}

public static List<String> parseCarNames(String rawCarNames) {
String[] tokens = rawCarNames.split(",");
List<String> parsedCarList = new ArrayList<>();

for (String token : tokens) {
String name = token.trim();

// 자동차 이름 검증 (빈 문자열, 공백, 5자 초과 여부)
CarNameValidator.validate(name);
parsedCarList.add(name);
}
return parsedCarList;
}

public static int parseAttemptCount(String rawAttemptCount) {
// 정수, 1 이상 여부 검증
AttemptCountValidator.validate(rawAttemptCount);
return Integer.parseInt(rawAttemptCount.trim());
}
}
Loading