Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
9fee122
docs: README.md 기능 요구사항 업데이트
Mar 14, 2026
1077534
feat: 자동차(Car) 도메인 모델 구현
Mar 15, 2026
5c22eae
feat: 자동차 이름과 시도횟수를 입력받는 기능 추가
Mar 15, 2026
4480a21
feat: controller에서 자동차와 시도횟수 입력 기능 추가
Mar 15, 2026
b566e31
feat: 자동차들 객체로 배열 만드는 기능 추가
Mar 15, 2026
159e617
refactor: racing으로 파일 이름 변경
Mar 15, 2026
1c4dfe5
feat: car 객체 이름 추가 기능 생성
Mar 15, 2026
7877f6c
fix: application 사용에서 psvm으 변경
Mar 15, 2026
11a0d07
feat: 자동차 객체 기능 추가
Mar 15, 2026
82e46fb
refactor: 잘못된 호출 수정
Mar 15, 2026
059c10a
feat: 자동차 이름을 검사하는 기능 추가
Mar 15, 2026
9dd00ba
feat: 경기마다 자동차 거리를 출력하고 최종 우승자를 출력하는 기능 추가
Mar 15, 2026
03accdd
feat: 경기를 진행하고 우승자를 뽑는 기능 추가
Mar 15, 2026
23abbbe
feat: 경기를 진행시키고 우승자를 출력하는 기능 추가
Mar 15, 2026
cb031fe
refactor: for문으로 출력하는 것이 아니라 자바 메서드로 출력하게 바꿈
Mar 15, 2026
bfc8e29
refactor: 초기값 0으로 수정
Mar 15, 2026
2fa8c64
feat: CarManager를 통하여 자동차 객체 관리
Mar 15, 2026
e1ad114
refactor: 오타수정
Mar 15, 2026
de9a01b
feat: 중복되는 자동차 이름 삭제 기능 추가
Mar 15, 2026
3f90b89
refactor: 출력형식 수정
Mar 15, 2026
f8eb16b
refactor: 모든 경우에서 출력하도록 변경
Mar 15, 2026
c79e89f
docs: 기능구현목록, 예외 처리 규칙, 프로그램 진행 방식 작성
Mar 16, 2026
0783491
refactor: 출력 로진 변경
Mar 18, 2026
00c6296
chore: 오타 수정
Mar 18, 2026
63eb12b
refactor: 참조자료형으로 변경
Mar 18, 2026
6f1f03f
refactor: Domain과 InputView 검증 분리
Mar 18, 2026
f0535c2
refactor: Domain과 InputView 검증 분리
Mar 18, 2026
480d3f6
feat: ,가 두변연속 나왔을 때 합치는 기능 추가
Mar 19, 2026
a3aae79
refactor: domain에서 -로 표시된 길을 outputView에서 출력
Mar 19, 2026
7ff14c6
refactor: 비지니스 규칙인 검증 수정
Mar 19, 2026
4e5c715
refactor: 사용하지 않는 메서드 삭제
Mar 19, 2026
beec64e
refactor: static 사용에서 객체 형태로 변경
Mar 19, 2026
2383aca
refactor: 초기값 0 삭제
Mar 19, 2026
f84be8a
refactor: 랜덤 발생을 검증하기 위해, static 사용에서 race 객체 사용
Mar 19, 2026
16f3a09
chore: 불필요한 공백제거
Mar 19, 2026
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
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# 🏎️ 자동차 경주 게임 (Car Racing)

## 🚀 기능 구현 목록

### 1. 입력 및 데이터 처리
- [ ] **자동차 이름 입력**: 쉼표(`,`)를 기준으로 구분하여 경주에 참여할 자동차 이름을 입력받는다.
- [ ] **데이터 변환**: 입력된 문자열을 분리하여 `Car` 객체 리스트로 생성한다.
- [ ] **시도 횟수 입력**: 전체 자동차가 이동을 시도할 총 횟수를 입력받는다.

### 2. 레이싱 로직
- [ ] **전진 조건 확인**: 매 라운드마다 각 자동차별로 무작위 값을 생성하여 전진 여부를 결정한다.
- [ ] **위치 업데이트**: 전진 조건을 만족하는 경우 자동차의 위치를 $1$ 씩 증가시킨다.
- [ ] **우승자 판별**: 모든 라운드 종료 후 전진 거리가 가장 긴 자동차를 우승자로 선정한다. (공동 우승 가능)

### 3. 출력
- [ ] **라운드 결과**: 매 라운드 종료 시점의 자동차별 이름과 전진 상태(`-`)를 출력한다.
- [ ] **최종 우승자**: 경주 종료 후 최종 우승자의 이름을 출력한다. (공동 우승 시 쉼표로 구분)

---

## ⚠️ 예외 처리 규칙 (Exception Handling)
잘못된 값 입력 시 `IllegalArgumentException`을 발생시키며, 프로그램은 즉시 종료되거나 에러 메시지를 출력해야 한다.

### [자동차 이름 관련]
1. **형식 오류**: 알파벳과 한글 이외의 문자(특수문자, 숫자 등)가 포함된 경우.
2. **공백 포함**: 이름 내부에 공백이 있거나, 입력값이 공백으로만 구성된 경우.
3. **입력 부재**: 자동차 이름을 입력하지 않고 진행하려 하는 경우.
4. **중복 발생**: 동일한 이름을 가진 자동차가 리스트 내에 중복으로 존재하는 경우.

### [시도 횟수 관련]
1. **타입 오류**: 숫자 이외의 문자, 특수기호, 공백이 포함된 경우.
2. **범위 오류**: 입력값이 $0$ 이하의 정수인 경우 (최소 $1$ 회 이상 필요).
3. **미입력**: 시도 횟수를 입력하지 않고 엔터를 입력한 경우.

---

## 💻 프로그램 진행 방식
1. **[Step 1]** `InputView`를 통한 자동차 이름 및 시도 횟수 입력.
2. **[Step 2]** 입력 데이터 검증 및 `List<Car>` 객체 생성.
3. **[Step 3]** 설정된 횟수만큼 경주 실행 및 실시간 결과 출력.
4. **[Step 4]** 최종 우승자 연산 및 `OutputView`를 통한 결과 발표.
31 changes: 31 additions & 0 deletions src/main/java/racing/controller/Controller.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package racing.controller;

import racing.domain.Car;
import racing.view.InputView;

import racing.domain.Cars;
import racing.domain.Race;
import racing.domain.RandomMoveStrategy;
import racing.view.OutputView;

public class Controller {
public static void main(String[] args) {
// [1] 자동차 이름 및 시도 횟수 입력 받기
String carNameInput = InputView.inputCarName();
int trialNumber = InputView.inputTrialNumberCount();
Race.validateTrialCount(trialNumber);
// [2] 데이터 변환: 문자열 -> 자동차 객체 리스트
Cars cars = new Cars(carNameInput);
Race race = new Race(cars);
//[3] 레이씽 경기 시작
RandomMoveStrategy moveStrategy = new RandomMoveStrategy();
for (int i = 0; i < trialNumber; i++) {
race.playRound(moveStrategy);
OutputView.printRoundResult(race.getParticipatingCars());
OutputView.println();
}
//[4] 결과 출력
OutputView.printWinners(race.getWinners());

}
}
38 changes: 38 additions & 0 deletions src/main/java/racing/domain/Car.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package racing.domain;

public class Car {
private final String name;
private int position;

public Car(String name) {
validateName(name);
this.name = name;
this.position = 0;
}
private void validateName(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("[ERROR] 자동차 이름은 1자 이상이어야 합니다.");
}
if (name.length() > 5) {
throw new IllegalArgumentException("[ERROR] 자동차 이름은 5자 이하여야 합니다.");
}
if (name.contains(" ")) {
throw new IllegalArgumentException("[ERROR] 자동차 이름에 공백을 포함할 수 없습니다.");
Comment on lines +19 to +20

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Readme에 남겨주신 내용처럼, 검증의 종류를 형식 검증과 나머지 비즈니스 규칙에 따른 검증을 나눠서 고민하신 것 같아요!

예를 들어, 쉼표가 연속해서 두번 나오는 경우(jun,,min)에 대한 검증과 이름길이에 대한 검증(예: sangjun - 5자 초과)는 검증의 종류가 다를텐데 모두 도메인에서 책임을 갖는 것이 맞을까요? 지금도 충분히 좋지만 검증을 어디서 하는지도 고민해보셨으면 좋겠습니다.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

맞습니다! 검증의 종류가 더 많아진다면, 형식과 비즈니스 규칙을 나눈 것처럼 역할과 기능을 분리하는 것이 정말 중요한 행동이라고 생각합니다 ! 😄

제가 검증에 대한 카테고리 분류에 대해서는 깊게 생각하지 못했다고 생각이 됩니다! 감사합니다 !

}
if (!name.matches("^[a-zA-Z0-9가-힣]*$")) {
throw new IllegalArgumentException("[ERROR] 자동차 이름에 특수문자를 포함할 수 없습니다.");
}
}
public void move() {
this.position++;
}
public String getName() {
return name;
}

public int getPosition() {
return position;
}


}
36 changes: 36 additions & 0 deletions src/main/java/racing/domain/Cars.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package racing.domain;

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

public class Cars {
private final List<Car> cars;

public Cars(String input) {
String[] names = input.split(",");
this.cars = new ArrayList<>();
for (String name : names) {
this.cars.add(new Car(name));
}
validateDuplicate();
}
private void validateDuplicate(){
long distinctCount = cars.stream()
.map(Car::getName) // Car 객체에서 이름을 추출한다고 가정
.distinct()
.count();
if (distinctCount != cars.size()) {
throw new IllegalArgumentException("[ERROR]중복된 자동차 이름이 존재합니다.");
}
}
public void moveAll(MoveStrategy moveStrategy) {
for (Car car : cars) {
if (moveStrategy.isMovable()) {
car.move();
}
}
}
public List<Car> getCarList(){
return cars;
}
}
5 changes: 5 additions & 0 deletions src/main/java/racing/domain/MoveStrategy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package racing.domain;

public interface MoveStrategy {
boolean isMovable();
}
34 changes: 34 additions & 0 deletions src/main/java/racing/domain/Race.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package racing.domain;

import java.util.List;
import java.util.stream.Collectors;
public class Race {
private final Cars cars;

public Race(Cars cars){
this.cars = cars;
}
public void playRound(MoveStrategy moveStrategy) {
cars.moveAll(moveStrategy);
}
public List<Car> getParticipatingCars() {
return cars.getCarList();
}
public static void validateTrialCount(int tryNumber){
if(tryNumber <= 0){
throw new IllegalArgumentException("[ERROR] 시도 횟수는 1회 이상이여야합니다.");
}
}
public List<String> getWinners() {
List<Car> carList = cars.getCarList();
int maxPosition = carList.stream()
.mapToInt(Car::getPosition)
.max()
.orElse(0);

return carList.stream()
.filter(car -> car.getPosition() == maxPosition)
.map(Car::getName)
.collect(Collectors.toList());
}
}
13 changes: 13 additions & 0 deletions src/main/java/racing/domain/RandomMoveStrategy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package racing.domain;

import java.util.Random;

public class RandomMoveStrategy implements MoveStrategy {
private static final int RANDOM_RANGE = 10;
private static final int MOVE_THRESHOLD = 4;
private static final Random random = new Random();
@Override
public boolean isMovable() {
return random.nextInt(RANDOM_RANGE) >= MOVE_THRESHOLD;
}
}
45 changes: 45 additions & 0 deletions src/main/java/racing/view/InputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package racing.view;
import java.util.Scanner;

public class InputView {

private static final String INPUT_CAR_NAME_MESSAGE = "경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분.";
private static final String INPUT_CAR_COUNT_MESSAGE = "시도할 회수는 몇회인가요?";
private static final Scanner scanner = new Scanner(System.in);

public static String inputCarName() {
System.out.println(INPUT_CAR_NAME_MESSAGE);
String carName = scanner.nextLine();
validateCarNameFormat(carName);

return carName;
}

public static int inputTrialNumberCount() {
System.out.println(INPUT_CAR_COUNT_MESSAGE);
String input = scanner.nextLine();
return parseTrialCount(input);
}

private static void validateCarNameFormat(String input){
if(input.isBlank()){
throw new IllegalArgumentException("[ERROR] 입력값이 없습니다.");
}
String noSpaceInput = input.replace(" ","");
if(noSpaceInput.contains(",,")){
throw new IllegalArgumentException("[ERROR] 쉼표가 연속으로 입력되었습니다.");
}
if(input.startsWith(",")||input.endsWith(",")){
throw new IllegalArgumentException("[ERROR] 입력값의 시작이나 끝에 쉼표가 있습니다.");
}

}
private static int parseTrialCount(String input) {
try {
return Integer.parseInt(input);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("[ERROR] 시도 횟수는 정수 형의 숫자여야 합니다.");
}
}
}

25 changes: 25 additions & 0 deletions src/main/java/racing/view/OutputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package racing.view;
import racing.domain.Car;
import java.util.List;

public class OutputView {
public static void printRoundResult(List<Car> cars) {
for (Car car : cars) {
System.out.println(car.getName() + " : " + "-".repeat(car.getPosition()));
}
}

public static void printWinners(List<String> winnerNames) {
if (winnerNames == null || winnerNames.isEmpty()) {
return;
}
String result = String.join(", ", winnerNames);
System.out.println(result + "가 최종 우승했습니다.");
}


public static void println(){
System.out.println();
}

}