Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
9fee122
docs: README.md 기능 요구사항 업데이트
mgim9316-a11y Mar 14, 2026
1077534
feat: 자동차(Car) 도메인 모델 구현
mgim9316-a11y Mar 15, 2026
5c22eae
feat: 자동차 이름과 시도횟수를 입력받는 기능 추가
mgim9316-a11y Mar 15, 2026
4480a21
feat: controller에서 자동차와 시도횟수 입력 기능 추가
mgim9316-a11y Mar 15, 2026
b566e31
feat: 자동차들 객체로 배열 만드는 기능 추가
mgim9316-a11y Mar 15, 2026
159e617
refactor: racing으로 파일 이름 변경
mgim9316-a11y Mar 15, 2026
1c4dfe5
feat: car 객체 이름 추가 기능 생성
mgim9316-a11y Mar 15, 2026
7877f6c
fix: application 사용에서 psvm으 변경
mgim9316-a11y Mar 15, 2026
11a0d07
feat: 자동차 객체 기능 추가
mgim9316-a11y Mar 15, 2026
82e46fb
refactor: 잘못된 호출 수정
mgim9316-a11y Mar 15, 2026
059c10a
feat: 자동차 이름을 검사하는 기능 추가
mgim9316-a11y Mar 15, 2026
9dd00ba
feat: 경기마다 자동차 거리를 출력하고 최종 우승자를 출력하는 기능 추가
mgim9316-a11y Mar 15, 2026
03accdd
feat: 경기를 진행하고 우승자를 뽑는 기능 추가
mgim9316-a11y Mar 15, 2026
23abbbe
feat: 경기를 진행시키고 우승자를 출력하는 기능 추가
mgim9316-a11y Mar 15, 2026
cb031fe
refactor: for문으로 출력하는 것이 아니라 자바 메서드로 출력하게 바꿈
mgim9316-a11y Mar 15, 2026
bfc8e29
refactor: 초기값 0으로 수정
mgim9316-a11y Mar 15, 2026
2fa8c64
feat: CarManager를 통하여 자동차 객체 관리
mgim9316-a11y Mar 15, 2026
e1ad114
refactor: 오타수정
mgim9316-a11y Mar 15, 2026
de9a01b
feat: 중복되는 자동차 이름 삭제 기능 추가
mgim9316-a11y Mar 15, 2026
3f90b89
refactor: 출력형식 수정
mgim9316-a11y Mar 15, 2026
f8eb16b
refactor: 모든 경우에서 출력하도록 변경
mgim9316-a11y Mar 15, 2026
c79e89f
docs: 기능구현목록, 예외 처리 규칙, 프로그램 진행 방식 작성
mgim9316-a11y Mar 16, 2026
0783491
refactor: 출력 로진 변경
mgim9316-a11y Mar 18, 2026
00c6296
chore: 오타 수정
mgim9316-a11y Mar 18, 2026
63eb12b
refactor: 참조자료형으로 변경
mgim9316-a11y Mar 18, 2026
6f1f03f
refactor: Domain과 InputView 검증 분리
mgim9316-a11y Mar 18, 2026
f0535c2
refactor: Domain과 InputView 검증 분리
mgim9316-a11y Mar 18, 2026
480d3f6
feat: ,가 두변연속 나왔을 때 합치는 기능 추가
mgim9316-a11y Mar 19, 2026
a3aae79
refactor: domain에서 -로 표시된 길을 outputView에서 출력
mgim9316-a11y Mar 19, 2026
7ff14c6
refactor: 비지니스 규칙인 검증 수정
mgim9316-a11y Mar 19, 2026
4e5c715
refactor: 사용하지 않는 메서드 삭제
mgim9316-a11y Mar 19, 2026
beec64e
refactor: static 사용에서 객체 형태로 변경
mgim9316-a11y Mar 19, 2026
2383aca
refactor: 초기값 0 삭제
mgim9316-a11y Mar 19, 2026
f84be8a
refactor: 랜덤 발생을 검증하기 위해, static 사용에서 race 객체 사용
mgim9316-a11y Mar 19, 2026
16f3a09
chore: 불필요한 공백제거
mgim9316-a11y Mar 19, 2026
c0ba151
refactor: application rhk controller 의 분리
mgim9316-a11y Mar 23, 2026
f7a77ae
refactor: Random인스턴스를 외부에서 입력 받게 수정, race 객체의 역할을 강화하기 위해 controller …
mgim9316-a11y Mar 23, 2026
0c1f264
chore: 오타 수정
mgim9316-a11y Mar 23, 2026
2e6d2cc
feat: random에 따라 움직이는 코드를 임의의 값을 주입하여 참/거짓을 올바르게 반환하는지 검토하는 테스트 코드 작성
mgim9316-a11y Mar 23, 2026
916bc1b
feat: input클래스 테스트 코드 작성
mgim9316-a11y Mar 23, 2026
75fbb12
refactor: scanner를 메서드 내부에서 호출, 지역 변수화
mgim9316-a11y Mar 23, 2026
1baf558
feat: 자동차 객체 테스트 파일 생성
mgim9316-a11y Mar 23, 2026
507359d
feat: 자동차 객체 테스트 파일 생성
mgim9316-a11y Mar 23, 2026
5b4a919
feat: 경기를 진행하는 테스트 파일 생성
mgim9316-a11y Mar 23, 2026
ffa3dcd
chore: 불필요한 주석 삭제
mgim9316-a11y Mar 23, 2026
d5bb220
chore: 사용하지 않는 헤더파일 삭제
mgim9316-a11y Mar 23, 2026
cccecbc
chore: 공백 추가
mgim9316-a11y Mar 23, 2026
1d18214
chore: 공백 추가
mgim9316-a11y Mar 23, 2026
5a2864e
chore: 공백 추가
mgim9316-a11y Mar 23, 2026
8830bf9
refactor: paring 메서드 분리
mgim9316-a11y Mar 26, 2026
4f4fbc2
refactor: outputView에서 입력 형식 출력
mgim9316-a11y Mar 26, 2026
a98da71
refactor: Controller에서 외부값 주입으로 변경
mgim9316-a11y Mar 26, 2026
39e70e3
refactor: 외부에서 리스트를 수정하지 못하도록 읽기 전용으로 수정
mgim9316-a11y Mar 26, 2026
03f95ee
refactor: 람다식을 활용하여 상태 조작 없이 완벽하게 제어되는 테스트 전용 MoveStrategy를 주입,
mgim9316-a11y Mar 26, 2026
56a6d53
refactor: 디스플레이 네임 컨벤션 수정
mgim9316-a11y Mar 26, 2026
8feb016
refactor: 공백 추가
mgim9316-a11y Mar 26, 2026
7622804
refactor: parse 메서드를 inputView 로 변경
mgim9316-a11y Mar 28, 2026
cd4eef1
chore: 불필요한 파일 삭제
mgim9316-a11y Mar 28, 2026
dac335b
refactor: println 기능 추가
mgim9316-a11y Mar 28, 2026
e631f1d
chore: 형식 수정
mgim9316-a11y Mar 28, 2026
ebf44d7
fix: 바뀐 계층 수정
mgim9316-a11y Mar 28, 2026
015f2c6
refactor: 파싱하는 기능과 배열을 만드는 기능을 분리
mgim9316-a11y Mar 28, 2026
9f5a01b
refactor: 레이스 하는 기능과 자동차를 앞으로 전진하는 기능 분리
mgim9316-a11y Mar 28, 2026
6a68d7f
refactor: 출력하는 기능은 연결함
mgim9316-a11y Mar 28, 2026
bff56d8
chore: DisplayName 과 형식 바뀐 자동차 배열 생성방식 적용
mgim9316-a11y Mar 28, 2026
3e5cc6c
chore: 공백 수정
mgim9316-a11y Mar 30, 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`를 통한 결과 발표.
9 changes: 9 additions & 0 deletions src/main/java/racing/Application.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package racing;
import racing.controller.Controller;

public class Application {
public static void main(String[] args) {
Controller controller = new Controller();
controller.run();
}
}
43 changes: 43 additions & 0 deletions src/main/java/racing/controller/Controller.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package racing.controller;

import racing.view.InputView;

import racing.domain.Cars;
import racing.domain.Race;
import racing.domain.Car;
import racing.view.OutputView;
import java.util.List;
import java.util.stream.Collectors;

import racing.domain.RandomMoveStrategy;

public class Controller {
public void run() {
// [1] 자동차 이름 및 시도 횟수 입력 받기
OutputView.printInputCarNameMessage();
String carNameInput = InputView.inputCarName();

OutputView.printInputTrialCountMessage();
int trialNumber = InputView.inputTrialNumberCount();
// [2] 데이터 변환: 문자열 -> 자동차 객체 리스트
List<String> carList = InputView.parse(carNameInput);
List<Car> raceCarList = carList.stream()
.map(Car::new)
.collect(Collectors.toList());
Cars cars = new Cars(raceCarList);

Race race = new Race(cars,trialNumber,new RandomMoveStrategy());
//[3] 레이씽 경기 시작
OutputView.println();
OutputView.printExecutionResultMessage();

while (race.hasMoreRounds()) {
race.playRound();
OutputView.printRoundResult(cars.getCarList());

}
//[4] 결과 출력
OutputView.printWinners(race.getWinners());

}
}
41 changes: 41 additions & 0 deletions src/main/java/racing/domain/Car.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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] 자동차 이름에 공백을 포함할 수 없습니다.");
}
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;
}


}
38 changes: 38 additions & 0 deletions src/main/java/racing/domain/Cars.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package racing.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;
validateDuplicate();
}
Comment on lines +6 to +12

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cars 일급 컬렉션에 대한 이야기는 이 코멘트를 참고해 주세요!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

넵 !


private void validateDuplicate() {
long distinctCount = cars.stream()
.map(Car::getName)
.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 Collections.unmodifiableList(cars);
}
Comment on lines +31 to +34

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

제 리뷰를 반영해 주셨군요 ~~! 👍

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

😄




}
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();
}
Comment on lines +3 to +5

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

👏👏

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

감사해요...ㅜㅜ

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

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

public class Race {
private final Cars cars;
private final int totalRounds;
private int currentRound;
private final MoveStrategy moveStrategy;

public Race(Cars cars, int totalRounds, MoveStrategy moveStrategy) {
if (totalRounds <= 0) {
throw new IllegalArgumentException("[ERROR] 시도 횟수는 1 이상이어야 합니다.");
}
this.cars = cars;
this.totalRounds = totalRounds;
this.currentRound = 0;
this.moveStrategy = moveStrategy;
}
Comment on lines +6 to +20

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MoveStrategy 전략을 Race내부가 아니라 외부에서 주입 받을 수 있도록 수정해 주셨군요!! 👏👏👏

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

💯 👍


public boolean hasMoreRounds() {
return currentRound < totalRounds;
}

public void playRound() {
if (!hasMoreRounds()) {
throw new IllegalStateException("[ERROR] 이미 모든 라운드가 종료되었습니다.");
}
cars.moveAll(moveStrategy);
currentRound++;
}

public List<String> getWinners() {
int maxPosition = cars.getCarList().stream()
.mapToInt(Car::getPosition)
.max()
.orElse(0);

return cars.getCarList().stream()
.filter(car -> car.getPosition() == maxPosition)
.map(Car::getName)
.collect(Collectors.toList());
}
}
23 changes: 23 additions & 0 deletions src/main/java/racing/domain/RandomMoveStrategy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
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 final Random random;

public RandomMoveStrategy() {
this(new Random());
}

public RandomMoveStrategy(Random random) {
this.random = random;
}

@Override
public boolean isMovable() {
return random.nextInt(RANDOM_RANGE) >= MOVE_THRESHOLD;
}
}

57 changes: 57 additions & 0 deletions src/main/java/racing/view/InputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package racing.view;

import racing.domain.Car;

import java.util.Scanner;




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



public class InputView {
private static final Scanner SCANNER = new Scanner(System.in);

public static String inputCarName() {
String carName = SCANNER.nextLine();
validateCarNameFormat(carName);
return carName;
}

public static int inputTrialNumberCount() {
String input = SCANNER.nextLine();
return parseTrialCount(input);
}
Comment on lines +25 to +28

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

입력의 책임을 갖는 InputView 부분에서 INPUT_CAR_COUNT_MESSAGE 출력 또한 진행하고 있는데,
이 출력문은 OutputView 로 이동시키는 건 어떨까요?

+) 이렇게 입력 부분에서 출력 문을 분리해 이동시키면 어떤 장점이 있는거 같으신가요?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

아무래도 지금 controller의 역할이 살짝 빈약하다는 생각이 됩니다.

적극적이기 controller 가 아닌 듯한 느낌을 받았습니다.

출력문을 분리해서 이동하시키게 된다면, 프로그램의 규모가 커지더라도, 수정이 비교적 수월해진다는 장점이 있습니다.

적극적인 controller 로 리팩토링을 하겠습니다!

제가 지금 코드를 봐도 outputView 가 domain 여기저기 흩어져 있는 것이 보이네요 ㅜ

이렇게된다면 outputView가 많아졌을 때, 어디서 리팩토링을 해야할지 못 찾을 수 있겠다는 생각을 했습니다.
숨어있으면 발견하기 어렵기 때문입니다 ㅜ.


private static void validateCarNameFormat(String input) {
if (input == null || 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] 입력값의 시작이나 끝에 쉼표가 있습니다.");
}
}
Comment on lines +30 to +41

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

여기 자동차 이름 입력을 검사하기위해 if 문으로 여러 케이스를 검증해 주시고 계신데요,

여기서도 Car 도메인 내부에서 자동차 이름을 검증했을때 처럼 정규표현식을 쓸 수 있었을 것 같은데,
이렇게 if 문을 나누어서 처리하게 된 이유가 있을까요??

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

view에서 검증하는 것과 domain에서 금증을 해야하는 기능을 분리했습니다!

inputView 같은 경우에는 숫자와 , 로 잘 되어있는지와 같은 입력된 형식에 대해서만을 검증을 하고 car 도메인에서는 정규식을 통해서 올바른 자동차의 이름을 검증하기 위해

다른 클래스로 분리하여 검증을 시도했습니다 !


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

public static List<String> parse(String input) {
return Arrays.stream(input.split(","))
.map(String::trim) // 양끝 공백 제거
.collect(Collectors.toList());
}

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

import racing.domain.Car;
import java.util.List;

public class OutputView {
private static final String INPUT_CAR_NAME_MESSAGE = "경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분).";
private static final String INPUT_CAR_COUNT_MESSAGE = "시도할 회수는 몇회인가요?";
private static final String EXECUTION_RESULT_MESSAGE = "실행 결과";

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

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

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

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

}

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();
}

}
Loading