diff --git a/docs/README.md b/docs/README.md
index e69de29..3941799 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -0,0 +1,206 @@
+# ⚾ Precourse-Week1 Mission **[숫자 야구]**
+
+## 💌 목차
+
+- [📦 패키지 구조](#패키지-구조)
+- [✨ 기능 구현 목록](#기능-구현-목록)
+- [🎨 구현 간 고민했던 내용들](#구현-간-고민했던-내용들)
+
+---
+
+## 📦 패키지 구조
+
+
+
+
+ | Package |
+ Class |
+ Description |
+
+
+ | 🕹 controller |
+ Game |
+ 게임 로직을 메인으로 동작하는 컨트롤러 클래스 |
+
+ |
+
+ | 💻 domain |
+ Number |
+ 사용자에게 입력받는 숫자와, 컴퓨터가 생성하는 숫자 클래스
+ |
+
+
+ | Result |
+ Ball Count와 Strike Count에 대한 결과 클래스 |
+
+
+ | ↘️ / constants |
+ ResultType |
+ 각 결과에 따라 다른 출력 방법에 대해 정의된 Enum |
+
+ |
+
+ | 📃 global |
+ GameConfig |
+ 전역으로 작용하는 설정과 제약조건에 대한 Enum |
+
+
+ | ↘️ / exception |
+ BaseballException |
+ 전역으로 처리하는 예외상황에 대한 Exception 클래스
|
+
+
+ | ErrorMessage |
+ 각 예외 상황에서 전역으로 던져질 예외의 메세지 Enum |
+
+ |
+
+ | ✅ validator |
+ InputValidator |
+ 사용자가 입력하는 숫자에 대한 제약조건 클래스 |
+
+ |
+
+ | 💬 view |
+ InputView |
+ 사용자 요청을 처리하는 클래스 |
+
+
+ | OutputView |
+ 사용자에게 응답을 출력하는 클래스 |
+
+
+ | ↘️ / constants |
+ StaticNotice |
+ 사용자에게 응답할 정적 메세지를 담은 열거형 클래스 |
+
+ |
+
+ | Package Structure Overview |
+
+
+  |
+
+
+
+
+
+---
+
+## ✨ 기능 구현 목록
+
+###
+
+- ✅ `a ~ b` 사이의 서로 값이 다른 `n자리`의 정수를 랜덤으로 생성한다.
+ - Default Setting : `1 ~ 9`사이의 서로 값이 다른 `3자리`의 정수
+- ✅ 게임 시작 문구 출력
+- ✅ 사용자에게 `a ~ b 사이의 서로 값이 다른 n자리의 정수`를 입력 받는다.
+ - 입력받은 input이 비어있을 경우 예외처리
+ - 입력받은 input이 숫자가 아닌 문자가 포함될 경우 예외처리
+ - 입력받은 input에 중복된 숫자가 있을 경우 예외처리
+- ✅ 사용자 input 숫자와 랜덤 생성 정수의 자리수를 비교해 출력할 힌트를 계산한다.
+ - 숫자의 값은 같지만 자리수가 다른 경우의 수 n개 : `n볼`
+ - 숫자의 값과 자리수가 모두 같은 경우의 수 m개 : `n스트라이크`
+- ✅계산된 힌트를 아래 양식으로 출력한다
+ - 볼 n개, 스트라이크 0개가 존재할 때 : `n볼`
+ - 볼 0개, 스트라이크 n개가 존재할 때 : `n스트라이크`
+ - 볼 n개, 스트라이크 m개가 존재할 때 : `n볼 m스트라이크`
+ - 볼 0개, 스트라이크 0개가 존재할 때 : `낫싱`
+
+- ✅ 게임 클리어 여부 판단
+ - `n스트라이크가 아니라면`, 다시 사용자에게 입력을 숫자를 받고, 힌트를 출력한다.
+ - `n스트라이크를 맞추었다면`, 아래 메세지를 출력하고 사용자에게 플래그를 입력받는다.
+ - `n개의 숫자를 모두 맞히셨습니다! 게임 종료`
+ - `게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요.`
+ - 입력받은 input이 1과 2가 아닌 숫자일 경우 예외처리
+ - 입력값에 따라 게임을 재시작하거나 종료한다.
+
+--------------------------------------------------------
+
+## 🎨 구현 간 고민했던 내용들
+
+### 1️⃣ 확장에는 열려있고, 변경에는 닫혀있는 OCP 설계
+
+- input 숫자의 범위가 변하더라도(1~9), 자리수가 변하더라도 대응이 손쉽게 가능해야 한다.
+ `GameConfig` 파일에서 `NUMBER_LENGTH` 변수의 value를 변경해 손쉽게 변경이 가능하다.
+ 개발 요구사항에서 자릿수 요청까지 처리하는 문제였다면, 더욱 OCP를 준수하는 코드 작성이 가능했을 것 같다.
+
+
+
+
+
+ | 숫자 3자리 |
+ 숫자 4자리 |
+ 숫자 5자리 |
+
+
+  |
+  |
+  |
+
+
+
+
+--------------------------------------------------------
+
+### 2️⃣ 4번의 대규모 리팩토링, 그리고 얻어낸 값진 `Number`
+
+- 영감을 얻게 해줬던 한 마디
+ ```bash
+ 객체는 '자율적인 존재'라는 점을 명심하라.
+ < 중략 >
+ 객체는 스스로의 행동에 의해서만 상태가 변경되는 것을 보장함으로써 객체의 자율성을 보장한다.
+
+ - 객체지향의 사실과 오해 中
+ ```
+
+- First-class collection + Static Factory Method 활용
+ ```java
+ public class Number {
+ private final List
numbers;
+
+ // Player Input Number Constructor
+ private Number(String input) {
+ validateEmpty(input);
+ validateNumberLength(input);
+ validateContainOnlyNumber(input);
+ validateContainDuplicatedNumber(input);
+
+ this.numbers = convertInputNumber(input);
+ }
+
+ // Computer Generated Number Constructor
+ private Number(List computerNumber) {
+ this.numbers = computerNumber;
+ }
+ }
+ ```
+
+- 일급 컬렉션과 생성자 검증을 사용해 `numbers`에 유효하게 검증이 끝난 숫자만 들어오도록 설계
+- playerNumber와, computerNumber의 생성자는 서로 다른 파라미터를 지니기 때문에, 개발자가 사용 간 혼동 가능
+ 해당 문제를 해결하기 위해, 생성자를 `private`으로 제한하고, 의미있는 메소드로만 생성자를 호출하도록 설계
+
+ ```java
+ // Computer Generated Number Constructor
+ public static Number generateRandomNumbers() {
+ List randomNumbers = new ArrayList<>();
+ while (randomNumbers.size() < NUMBER_LENGTH.getValue()) {
+ int number = pickNumberInRange(RANDOM_NUMBER_MINIMUM.getValue(), RANDOM_NUMBER_MAXIMUM.getValue());
+ if (!hasDuplicatedNumber(randomNumbers, number)) {
+ randomNumbers.add(number);
+ }
+ }
+ return new Number(randomNumbers);
+ }
+
+ // Player Number Static Factory Method
+ public static Number inputPlayerNumbers() {
+ String playerNumbers = InputView.askPlayerNumbers();
+ return new Number(playerNumbers);
+ }
+ ```
+- 정적 팩토리 메소드 명에 의미를 부여하고, 개발자가 직관적으로 해석할 수 있도록 했고,
+ 일급 컬렉션을 활용해 검증이 끝난 유효한 값만 리스트에 담을 수 있게 되었다!
+
+-
+
diff --git a/src/main/java/baseball/Application.java b/src/main/java/baseball/Application.java
index dd95a34..0443ab5 100644
--- a/src/main/java/baseball/Application.java
+++ b/src/main/java/baseball/Application.java
@@ -1,7 +1,10 @@
package baseball;
+import baseball.controller.Game;
+
public class Application {
public static void main(String[] args) {
- // TODO: 프로그램 구현
+ Game game = new Game();
+ game.start();
}
}
diff --git a/src/main/java/baseball/controller/Game.java b/src/main/java/baseball/controller/Game.java
new file mode 100644
index 0000000..3ef9dec
--- /dev/null
+++ b/src/main/java/baseball/controller/Game.java
@@ -0,0 +1,36 @@
+package baseball.controller;
+
+import baseball.domain.Number;
+import baseball.domain.Result;
+import baseball.validator.InputValidator;
+import baseball.view.InputView;
+import baseball.view.OutputView;
+
+import static baseball.view.OutputView.printStaticNotice;
+import static baseball.view.constants.StaticNotice.GAME_START;
+
+public class Game {
+ public void start() {
+ printStaticNotice(GAME_START);
+ do {
+ Number computerNumber = Number.generateRandomNumbers();
+ play(computerNumber);
+ } while (!askRestartOrExit());
+ }
+
+ private void play(Number computerNumber) {
+ while (true) {
+ Number playerNumber = Number.inputPlayerNumbers();
+ Result result = Result.create(playerNumber, computerNumber);
+ OutputView.printMessage(result.createResultMessage());
+ if (result.checkGameOver()) {
+ break;
+ }
+ }
+ }
+
+ private boolean askRestartOrExit() {
+ String input = InputView.askRestartOrExit();
+ return InputValidator.isValidRestartFlag(input);
+ }
+}
diff --git a/src/main/java/baseball/domain/Number.java b/src/main/java/baseball/domain/Number.java
new file mode 100644
index 0000000..df619ae
--- /dev/null
+++ b/src/main/java/baseball/domain/Number.java
@@ -0,0 +1,80 @@
+package baseball.domain;
+
+import baseball.view.InputView;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.IntStream;
+
+import static baseball.global.GameConfig.*;
+import static baseball.validator.InputValidator.*;
+import static camp.nextstep.edu.missionutils.Randoms.pickNumberInRange;
+
+public class Number {
+ private final List numbers;
+
+ // Player Number Constructor
+ private Number(String input) {
+ validateEmpty(input);
+ validateNumberLength(input);
+ validateContainOnlyNumber(input);
+ validateContainDuplicatedNumber(input);
+
+ this.numbers = convertInputNumber(input);
+ }
+
+ // Computer Number Constructor
+ private Number(List computerNumber) {
+ this.numbers = computerNumber;
+ }
+
+ // Player Number Static Factory Method
+ public static Number inputPlayerNumbers() {
+ String playerNumbers = InputView.askPlayerNumbers();
+ return new Number(playerNumbers);
+ }
+
+ // Computer Number Static Factory Method
+ public static Number generateRandomNumbers() {
+ List randomNumbers = new ArrayList<>();
+ while (randomNumbers.size() < NUMBER_LENGTH.getValue()) {
+ int number = pickNumberInRange(RANDOM_NUMBER_MINIMUM.getValue(), RANDOM_NUMBER_MAXIMUM.getValue());
+ if (!hasDuplicatedNumber(randomNumbers, number)) {
+ randomNumbers.add(number);
+ }
+ }
+ return new Number(randomNumbers);
+ }
+
+ private static boolean hasDuplicatedNumber(List randomNumbers, int number) {
+ return randomNumbers.contains(number);
+ }
+
+ private List convertInputNumber(String input) {
+ return input.chars()
+ .mapToObj(Character::getNumericValue)
+ .toList();
+ }
+
+ public int countBallCount(final Number comparableNumber) {
+ return (int) IntStream.range(0, numbers.size())
+ .filter(i -> comparableNumber.isBall(numbers.get(i), i))
+ .count();
+ }
+
+ public int countStrikeCount(final Number comparableNumber) {
+ return (int) IntStream.range(0, numbers.size())
+ .filter(i -> comparableNumber.isStrike(numbers.get(i), i))
+ .count();
+ }
+
+ // Ball : Contain their number at other position
+ private boolean isBall(int number, int digit) {
+ return !isStrike(number, digit) && numbers.contains(number);
+ }
+
+ // Strike : Contain their number at same digit
+ private boolean isStrike(int number, int digit) {
+ return number == numbers.get(digit);
+ }
+}
diff --git a/src/main/java/baseball/domain/Result.java b/src/main/java/baseball/domain/Result.java
new file mode 100644
index 0000000..8d5195d
--- /dev/null
+++ b/src/main/java/baseball/domain/Result.java
@@ -0,0 +1,73 @@
+package baseball.domain;
+
+import baseball.domain.constants.ResultType;
+import baseball.global.exception.BaseballException;
+
+import static baseball.domain.constants.ResultType.*;
+import static baseball.global.GameConfig.NUMBER_LENGTH;
+import static baseball.global.exception.ErrorMessage.SYSTEM_ERROR;
+import static java.lang.String.format;
+
+public class Result {
+ private final int ballCount;
+ private final int strikeCount;
+
+ private Result(final Number playerNumber, final Number computerNumber) {
+ ballCount = playerNumber.countBallCount(computerNumber);
+ strikeCount = playerNumber.countStrikeCount(computerNumber);
+ }
+
+ public static Result create(final Number playerNumber, final Number computerNumber) {
+ return new Result(playerNumber, computerNumber);
+ }
+
+ private ResultType inspectResultType() {
+ if (isNothing()) {
+ return NOTHING;
+ }
+ if (hasBallAndStrike()) {
+ return BALL_AND_STRIKE;
+ }
+ if (hasBall()) {
+ return ONLY_BALL;
+ }
+ if (hasStrike()) {
+ return ONLY_STRIKE;
+ }
+ throw BaseballException.of(SYSTEM_ERROR);
+ }
+
+ private boolean hasBallAndStrike() {
+ return hasBall() && hasStrike();
+ }
+
+ private boolean isNothing() {
+ return !hasBall() && !hasStrike();
+ }
+
+ private String generateResultMessage(ResultType resultType) {
+ return switch (resultType) {
+ case NOTHING -> resultType.getValue();
+ case BALL_AND_STRIKE -> format(BALL_AND_STRIKE.getValue(), ballCount, strikeCount);
+ case ONLY_BALL -> format(ONLY_BALL.getValue(), ballCount);
+ case ONLY_STRIKE -> format(ONLY_STRIKE.getValue(), strikeCount);
+ };
+ }
+
+ public String createResultMessage() {
+ ResultType resultType = inspectResultType();
+ return generateResultMessage(resultType);
+ }
+
+ public boolean hasBall() {
+ return ballCount > 0;
+ }
+
+ public boolean hasStrike() {
+ return strikeCount > 0;
+ }
+
+ public boolean checkGameOver() {
+ return strikeCount == NUMBER_LENGTH.getValue();
+ }
+}
diff --git a/src/main/java/baseball/domain/constants/ResultType.java b/src/main/java/baseball/domain/constants/ResultType.java
new file mode 100644
index 0000000..7f17806
--- /dev/null
+++ b/src/main/java/baseball/domain/constants/ResultType.java
@@ -0,0 +1,19 @@
+package baseball.domain.constants;
+
+public enum ResultType {
+
+ NOTHING("낫싱"),
+ ONLY_BALL("%d볼"),
+ ONLY_STRIKE("%d스트라이크"),
+ BALL_AND_STRIKE("%d볼 %d스트라이크");
+
+ private final String value;
+
+ ResultType(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+}
diff --git a/src/main/java/baseball/global/GameConfig.java b/src/main/java/baseball/global/GameConfig.java
new file mode 100644
index 0000000..6952f68
--- /dev/null
+++ b/src/main/java/baseball/global/GameConfig.java
@@ -0,0 +1,25 @@
+package baseball.global;
+
+public enum GameConfig {
+ NUMBER_LENGTH(3),
+ RANDOM_NUMBER_MINIMUM(1),
+ RANDOM_NUMBER_MAXIMUM(9),
+
+ EXIT_FLAG(1),
+ RESTART_FLAG(2);
+
+ private final int value;
+
+ GameConfig(int value) {
+ this.value = value;
+ }
+
+ @Override
+ public String toString() {
+ return String.valueOf(value);
+ }
+
+ public int getValue() {
+ return value;
+ }
+}
diff --git a/src/main/java/baseball/global/exception/BaseballException.java b/src/main/java/baseball/global/exception/BaseballException.java
new file mode 100644
index 0000000..77c784a
--- /dev/null
+++ b/src/main/java/baseball/global/exception/BaseballException.java
@@ -0,0 +1,11 @@
+package baseball.global.exception;
+
+public class BaseballException extends IllegalArgumentException {
+ private BaseballException(ErrorMessage errorMessage) {
+ super(errorMessage.getMessage());
+ }
+
+ public static BaseballException of(ErrorMessage errorMessage) {
+ return new BaseballException(errorMessage);
+ }
+}
diff --git a/src/main/java/baseball/global/exception/ErrorMessage.java b/src/main/java/baseball/global/exception/ErrorMessage.java
new file mode 100644
index 0000000..30e4f0b
--- /dev/null
+++ b/src/main/java/baseball/global/exception/ErrorMessage.java
@@ -0,0 +1,20 @@
+package baseball.global.exception;
+
+public enum ErrorMessage {
+ INVALID_LENGTH("The input length cannot be different from length configured by the game."),
+ DUPLICATED_NUMBER("The input cannot contain duplicated numbers."),
+ CONTAIN_LETTER("The input cannot contain letters."),
+ EMPTY_NUMBER("The input cannot be empty."),
+ INVALID_FLAG("The input cannot be different flag from configured by the game."),
+ SYSTEM_ERROR("The game system has crashed");
+
+ private final String message;
+
+ ErrorMessage(String message) {
+ this.message = message;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+}
diff --git a/src/main/java/baseball/validator/InputValidator.java b/src/main/java/baseball/validator/InputValidator.java
new file mode 100644
index 0000000..12d7cd5
--- /dev/null
+++ b/src/main/java/baseball/validator/InputValidator.java
@@ -0,0 +1,76 @@
+package baseball.validator;
+
+
+import baseball.global.exception.BaseballException;
+
+import static baseball.global.GameConfig.*;
+import static baseball.global.exception.ErrorMessage.*;
+
+public class InputValidator {
+ protected InputValidator() {
+ }
+
+ public static boolean isValidRestartFlag(String flag) {
+ validateGameFlag(flag);
+ return flag.equals(RESTART_FLAG.toString());
+ }
+
+ private static void validateGameFlag(final String flag) {
+ if (!isExitFlag(flag) && !isRestartFlag(flag)) {
+ throw BaseballException.of(INVALID_FLAG);
+ }
+ }
+
+ private static boolean isExitFlag(String flag) {
+ return flag.equals(EXIT_FLAG.toString());
+ }
+
+ private static boolean isRestartFlag(String flag) {
+ return flag.equals(RESTART_FLAG.toString());
+ }
+
+
+ public static void validateNumberLength(final String number) {
+ if (!isValidLength(number)) {
+ throw BaseballException.of(INVALID_LENGTH);
+ }
+ }
+
+ public static void validateContainDuplicatedNumber(final String number) {
+ if (!isUniqueNumber(number)) {
+ throw BaseballException.of(DUPLICATED_NUMBER);
+ }
+ }
+
+ public static void validateContainOnlyNumber(final String number) {
+ if (!isValidNumber(number)) {
+ throw BaseballException.of(CONTAIN_LETTER);
+ }
+ }
+
+ public static void validateEmpty(final String number) {
+ if (isEmpty(number)) {
+ throw BaseballException.of(EMPTY_NUMBER);
+ }
+ }
+
+ private static boolean isEmpty(String number) {
+ return number.isEmpty();
+ }
+
+ private static boolean isValidLength(final String number) {
+ return number.length() == NUMBER_LENGTH.getValue();
+ }
+
+ private static boolean isValidNumber(final String number) {
+ return number
+ .chars()
+ .allMatch(c -> Character.isDigit(c) && c >= '1' && c <= '9');
+ }
+
+ private static boolean isUniqueNumber(final String number) {
+ return number.chars()
+ .distinct()
+ .count() == number.length();
+ }
+}
diff --git a/src/main/java/baseball/view/InputView.java b/src/main/java/baseball/view/InputView.java
new file mode 100644
index 0000000..a4216b1
--- /dev/null
+++ b/src/main/java/baseball/view/InputView.java
@@ -0,0 +1,18 @@
+package baseball.view;
+
+import static baseball.view.OutputView.printStaticNotice;
+import static baseball.view.constants.StaticNotice.*;
+import static camp.nextstep.edu.missionutils.Console.readLine;
+
+public class InputView {
+ public static String askPlayerNumbers() {
+ printStaticNotice(ASK_PLAYER_NUMBER);
+ return readLine();
+ }
+
+ public static String askRestartOrExit() {
+ printStaticNotice(GAME_OVER);
+ printStaticNotice(ASK_RESTART_OR_EXIT);
+ return readLine();
+ }
+}
diff --git a/src/main/java/baseball/view/OutputView.java b/src/main/java/baseball/view/OutputView.java
new file mode 100644
index 0000000..cdf550e
--- /dev/null
+++ b/src/main/java/baseball/view/OutputView.java
@@ -0,0 +1,13 @@
+package baseball.view;
+
+import baseball.view.constants.StaticNotice;
+
+public class OutputView {
+ public static void printStaticNotice(StaticNotice notice) {
+ System.out.print(notice.getMessage());
+ }
+
+ public static void printMessage(String message) {
+ System.out.println(message);
+ }
+}
diff --git a/src/main/java/baseball/view/constants/StaticNotice.java b/src/main/java/baseball/view/constants/StaticNotice.java
new file mode 100644
index 0000000..6bc269d
--- /dev/null
+++ b/src/main/java/baseball/view/constants/StaticNotice.java
@@ -0,0 +1,21 @@
+package baseball.view.constants;
+
+import static baseball.global.GameConfig.NUMBER_LENGTH;
+import static java.lang.String.format;
+
+public enum StaticNotice {
+ GAME_START("숫자 야구 게임을 시작합니다.\n"),
+ ASK_PLAYER_NUMBER("숫자를 입력해주세요 : "),
+ ASK_RESTART_OR_EXIT("게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요.\n"),
+ GAME_OVER(format("%d개의 숫자를 모두 맞히셨습니다! 게임 종료%n", NUMBER_LENGTH.getValue()));
+
+ private final String message;
+
+ StaticNotice(String message) {
+ this.message = message;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+}
diff --git a/src/test/java/baseball/ApplicationTest.java b/src/test/java/baseball/ApplicationTest.java
index 3fa29fa..fcd4cde 100644
--- a/src/test/java/baseball/ApplicationTest.java
+++ b/src/test/java/baseball/ApplicationTest.java
@@ -1,13 +1,13 @@
package baseball;
-import camp.nextstep.edu.missionutils.test.NsTest;
-import org.junit.jupiter.api.Test;
-
import static camp.nextstep.edu.missionutils.test.Assertions.assertRandomNumberInRangeTest;
import static camp.nextstep.edu.missionutils.test.Assertions.assertSimpleTest;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import camp.nextstep.edu.missionutils.test.NsTest;
+import org.junit.jupiter.api.Test;
+
class ApplicationTest extends NsTest {
@Test
void 게임종료_후_재시작() {