From a2a9bcd1de1c480fc469108d332256fbf89c201b Mon Sep 17 00:00:00 2001
From: myeongheon <axm0219@naver.com>
Date: Tue, 7 May 2024 19:35:14 +0900
Subject: [PATCH 01/10] docs: update README.md

---
 README.md | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 91 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 8d7e8aee..effb309e 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,91 @@
-# java-baseball-precourse
\ No newline at end of file
+# 숫자 야구
+
+1부터 9까지 서로 다른 수로 이루어진 3자리의 수를 맞추는 게임이다.
+
+## 기능 요구 사항
+
+---
+
+- 같은 수가 같은 자리에 있으면 스트라이크, 다른 자리에 있으면 볼, 같은 수가 전혀 없으면 낫싱이란 힌트를 얻고, 그 힌트를 이용해서 먼저 상대방(컴퓨터)의 수를 맞추면 승리한다.
+
+```markdown
+e.g.
+상대방(컴퓨터)의 수가 425일 때,
+- 123을 제시한 경우 : 1스트라이크
+- 456을 제시한 경우 : 1볼 1스트라이크
+- 789를 제시한 경우 : 낫싱
+```
+
+- 위 숫자 야구 게임에서 상대방의 역할을 컴퓨터가 한다. 컴퓨터는 1에서 9까지 서로 다른 임의의 수 3개를 선택한다. 게임 플레이어는 컴퓨터가 생각하고 있는 3개의 숫자를 입력하고, 컴퓨터는 입력한 숫자에 대한 결과를 출력한다.
+- 이 같은 과정을 반복해 컴퓨터가 선택한 3개의 숫자를 모두 맞히면 게임이 종료된다.
+- 게임을 종료한 후 게임을 다시 시작하거나 완전히 종료할 수 있다.
+- 사용자가 잘못된 값을 입력할 경우 IllegalArgumentException을 발생시킨 후 애플리케이션은 종료되어야 한다.
+
+## 실행 결과
+
+---
+
+```markdown
+숫자를 입력해 주세요 : 123
+1볼 1스트라이크
+숫자를 입력해 주세요 : 145
+1볼
+숫자를 입력해 주세요 : 671
+2볼
+숫자를 입력해 주세요 : 216
+1스트라이크
+숫자를 입력해 주세요 : 713
+3스트라이크
+3개의 숫자를 모두 맞히셨습니다! 게임 종료
+게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요.
+1
+숫자를 입력해 주세요 : 123
+1볼
+...
+```
+
+- 프로그래밍 요구 사항 1
+    - JDK 17 버전에서 실행 가능해야 한다.
+    - 프로그램 실행의 시작점은 Application 의 main() 이다.
+    - build.gradle 파일은 변경할 수 없으며, 제공된 라이브러리 이외의 외부 라이브러리는 사용하지 않는다.
+    - 프로그램 종료 시 System.exit() 를 호출하지 않는다.
+    - 프로그래밍 요구 사항에서 달리 명시하지 않는 한 파일, 패키지 등의 이름을 바꾸거나 이동하지 않는다.
+- 프로그래밍 요구 사항 2
+    - 자바 코드 컨벤션을 지키면서 프로그래밍한다.
+        - 기본적으로 Google Java Style Guide을 원칙으로 한다.
+        - 단, 들여쓰기는 '2 spaces'가 아닌 '4 spaces'로 한다.
+    - indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다.
+        - 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.
+        - 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면 된다.
+    - 3항 연산자를 쓰지 않는다.
+    - 함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라.
+    - JUnit 5와 AssertJ를 이용하여 정리한 기능 목록이 정상적으로 작동하는지 테스트 코드로 확인한다.
+    - 테스트 도구 사용법이 익숙하지 않다면 아래 문서를 참고하여 학습한 후 테스트를 구현한다.
+        - JUnit 5 User Guide
+        - AssertJ User Guide
+        - AssertJ Exception Assertions
+        - Guide to JUnit 5 Parameterized Test
+
+## 기능 목록
+
+---
+
+1. 컴퓨터는 1에서 9까지 서로 다른 임의의 수 3개를 선택한다.
+2. 사용자로부터 3자리의 수를 입력 받는다.
+    - 사용자가 잘못된 값을 입력할 경우 예외 발생 후 실행 종료
+        - 3자리가 아닌 경우
+        - 숫자가 아닌 경우
+        - 0이 입력된 경우
+        - 중복된 숫자가 입력된 경우
+3. 입력 값에 따라 힌트를 제공한다.
+    - 동일한 자리 - 동일한 수 → 스트라이크
+    - 다른 자리 - 동일한 수 → 볼
+    - 다른 수 → 낫싱
+4. 정답을 맞추면 게임을 종료한다.
+    - 3스트라이크인 경우 → 게임 종료
+    - 3스트라이크가 아닌 경우 → 게임 진행
+5. 게임 종료 후, 게임을 다시 시작하거나 완전히 종료할 수 있다.
+    - 1 입력 → 게임 새로 시작
+    - 2 입력 → 게임 완전히 종료
+    - 사용자가 잘못된 값을 입력할 경우 예외 발생 후 실행 종료
+        - 1, 2가 아닌 값을 입력할 경우
\ No newline at end of file

From e353ba6c0ed8bac1dd2397b80e1fb0eb05408006 Mon Sep 17 00:00:00 2001
From: myeongheon <axm0219@naver.com>
Date: Tue, 7 May 2024 23:51:48 +0900
Subject: [PATCH 02/10] =?UTF-8?q?feat:=20=EC=BB=B4=ED=93=A8=ED=84=B0=203?=
 =?UTF-8?q?=EC=9E=90=EB=A6=AC=20=EC=88=AB=EC=9E=90=20=EC=83=9D=EC=84=B1=20?=
 =?UTF-8?q?=EA=B8=B0=EB=8A=A5=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../java/baseball/domain/NumberGenerator.java   | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)
 create mode 100644 src/main/java/baseball/domain/NumberGenerator.java

diff --git a/src/main/java/baseball/domain/NumberGenerator.java b/src/main/java/baseball/domain/NumberGenerator.java
new file mode 100644
index 00000000..7f964bd1
--- /dev/null
+++ b/src/main/java/baseball/domain/NumberGenerator.java
@@ -0,0 +1,17 @@
+package baseball.domain;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class NumberGenerator {
+
+    public static String generateNumber() {
+        List<Integer> numbers = new ArrayList<>();
+        for (int i = 1; i <= 9; i++) {
+            numbers.add(i);
+        }
+        Collections.shuffle(numbers);
+        return "" + numbers.get(0) + numbers.get(1) + numbers.get(2);
+    }
+}

From 9165b11c6a20a51b5b1dbb5e09e9df165d2c340e Mon Sep 17 00:00:00 2001
From: myeongheon <axm0219@naver.com>
Date: Tue, 7 May 2024 23:53:40 +0900
Subject: [PATCH 03/10] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?=
 =?UTF-8?q?=EC=9E=85=EB=A0=A5=EA=B0=92=20=EA=B4=80=EB=A0=A8=20=EA=B8=B0?=
 =?UTF-8?q?=EB=8A=A5=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/main/java/baseball/domain/GameRules.java | 42 ++++++++++++++++++++
 1 file changed, 42 insertions(+)
 create mode 100644 src/main/java/baseball/domain/GameRules.java

diff --git a/src/main/java/baseball/domain/GameRules.java b/src/main/java/baseball/domain/GameRules.java
new file mode 100644
index 00000000..f297ddb5
--- /dev/null
+++ b/src/main/java/baseball/domain/GameRules.java
@@ -0,0 +1,42 @@
+package baseball.domain;
+
+public class GameRules {
+    public String generateHint(String secretNumber, String userGuess) {
+        int strike = 0;
+        int ball = 0;
+        for (int i = 0; i < 3; i++) {
+            if (userGuess.charAt(i) == secretNumber.charAt(i)) {
+                strike++;
+            } else if (secretNumber.contains(userGuess.substring(i, i+1))) {
+                ball++;
+            }
+        }
+        return createHintMessage(strike, ball);
+    }
+
+    private String createHintMessage(int strike, int ball) {
+        String result = "";
+        if (strike == 0 && ball == 0) {
+            result = "낫싱";
+        } else {
+            if (ball > 0) {
+                result += ball + "볼 ";
+            }
+            if (strike > 0) {
+                result += strike + "스트라이크";
+            }
+        }
+        return result.trim();
+    }
+
+    public Boolean decideRestart(String input) {
+        switch (input) {
+            case "1":
+                return true;
+            case "2":
+                return false;
+            default:
+                return null;
+        }
+    }
+}

From bbd60c69719740a075a6da22b621fb4a6bfd00e1 Mon Sep 17 00:00:00 2001
From: myeongheon <axm0219@naver.com>
Date: Tue, 7 May 2024 23:54:33 +0900
Subject: [PATCH 04/10] =?UTF-8?q?feat:=20=EC=9E=85=EB=A0=A5=EA=B0=92=20?=
 =?UTF-8?q?=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC=20=EA=B8=B0?=
 =?UTF-8?q?=EB=8A=A5=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../java/baseball/game/InputValidator.java    | 32 +++++++++++++++++++
 1 file changed, 32 insertions(+)
 create mode 100644 src/main/java/baseball/game/InputValidator.java

diff --git a/src/main/java/baseball/game/InputValidator.java b/src/main/java/baseball/game/InputValidator.java
new file mode 100644
index 00000000..db5c63e6
--- /dev/null
+++ b/src/main/java/baseball/game/InputValidator.java
@@ -0,0 +1,32 @@
+package baseball.game;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public class InputValidator {
+    public static boolean isValidInput(String input) {
+        return isProperLength(input) && isAllDigits(input) && hasNoZero(input) && hasUniqueDigits(input);
+    }
+
+    private static boolean isProperLength(String input) {
+        return input.length() == 3;
+    }
+
+    private static boolean isAllDigits(String input) {
+        return input.chars().allMatch(Character::isDigit);
+    }
+
+    private static boolean hasNoZero(String input) {
+        return !input.contains("0");
+    }
+
+    private static boolean hasUniqueDigits(String input) {
+        Set<Character> digits = new HashSet<>();
+        for (char c : input.toCharArray()) {
+            if (!digits.add(c)) {
+                return false;
+            }
+        }
+        return true;
+    }
+}

From 442bb059f67b99afe13c6c8f5966390b52473605 Mon Sep 17 00:00:00 2001
From: myeongheon <axm0219@naver.com>
Date: Tue, 7 May 2024 23:55:18 +0900
Subject: [PATCH 05/10] =?UTF-8?q?feat:=20=EA=B2=8C=EC=9E=84=20=ED=99=94?=
 =?UTF-8?q?=EB=A9=B4=20=EA=B8=B0=EB=8A=A5=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/main/java/baseball/view/GameView.java | 33 +++++++++++++++++++++++
 1 file changed, 33 insertions(+)
 create mode 100644 src/main/java/baseball/view/GameView.java

diff --git a/src/main/java/baseball/view/GameView.java b/src/main/java/baseball/view/GameView.java
new file mode 100644
index 00000000..10400d8a
--- /dev/null
+++ b/src/main/java/baseball/view/GameView.java
@@ -0,0 +1,33 @@
+package baseball.view;
+
+import java.util.Scanner;
+
+public class GameView {
+    private Scanner scanner;
+
+    public GameView() {
+        this.scanner = new Scanner(System.in);
+    }
+
+    public String promptForGuess() {
+        System.out.println("숫자를 입력해 주세요: ");
+        return scanner.nextLine();
+    }
+
+    public void displayResult(String result) {
+        System.out.println(result);
+    }
+
+    public void displayGameEnd() {
+        System.out.println("3개의 숫자를 모두 맞히셨습니다! 게임 종료");
+    }
+
+    public String promptForRestart() {
+        System.out.println("게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요.");
+        return scanner.nextLine();
+    }
+
+    public void displayError(String message) {
+        System.out.println(message);
+    }
+}

From 40b6fe938e0faa8be7a2863faa230959ea451852 Mon Sep 17 00:00:00 2001
From: myeongheon <axm0219@naver.com>
Date: Tue, 7 May 2024 23:55:50 +0900
Subject: [PATCH 06/10] =?UTF-8?q?feat:=20=EA=B2=8C=EC=9E=84=EC=A7=84?=
 =?UTF-8?q?=ED=96=89=20=EA=B8=B0=EB=8A=A5=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/main/java/baseball/game/Game.java | 63 +++++++++++++++++++++++++++
 1 file changed, 63 insertions(+)
 create mode 100644 src/main/java/baseball/game/Game.java

diff --git a/src/main/java/baseball/game/Game.java b/src/main/java/baseball/game/Game.java
new file mode 100644
index 00000000..4fc8ba5b
--- /dev/null
+++ b/src/main/java/baseball/game/Game.java
@@ -0,0 +1,63 @@
+package baseball.game;
+
+import baseball.domain.GameRules;
+import baseball.domain.NumberGenerator;
+import baseball.view.GameView;
+
+public class Game {
+    private GameView gameView;
+    private NumberGenerator numberGenerator;
+    private GameRules gameRules;
+    private String computerNumber;
+
+    public Game() {
+        this.gameView = new GameView();
+        this.numberGenerator = new NumberGenerator();
+        this.gameRules = new GameRules();
+        this.computerNumber = numberGenerator.generateNumber();
+    }
+
+    public void startGame() {
+        boolean gameContinues = true;
+        try {
+            while (gameContinues) {
+                gameContinues = game();
+            }
+        } catch (IllegalArgumentException e) {
+            gameView.displayError(e.getMessage());
+        }
+    }
+
+    private boolean game() {
+        String userGuess = gameView.promptForGuess();
+        if (!InputValidator.isValidInput(userGuess)) {
+            throw new IllegalArgumentException("잘못된 값을 입력했습니다.");
+        }
+        String result = gameRules.generateHint(computerNumber, userGuess);
+        gameView.displayResult(result);
+        if (result.equals("3스트라이크")) {
+            gameView.displayGameEnd();
+            return promptForRestart();
+        }
+        return true;
+    }
+
+    private boolean promptForRestart() {
+        while (true) {
+            String input = gameView.promptForRestart();
+            Boolean decision = gameRules.decideRestart(input);
+            if (decision == null) {
+                throw new IllegalArgumentException("잘못된 값을 입력했습니다.");
+            } else {
+                if (decision) {
+                    resetGame();
+                }
+                return decision;
+            }
+        }
+    }
+
+    private void resetGame() {
+        computerNumber = numberGenerator.generateNumber();
+    }
+}

From 69caaed96b88fd8031cc55ed0d307b887493a606 Mon Sep 17 00:00:00 2001
From: myeongheon <axm0219@naver.com>
Date: Tue, 7 May 2024 23:56:12 +0900
Subject: [PATCH 07/10] =?UTF-8?q?feat:=20=EB=A9=94=EC=9D=B8=20=EC=8B=A4?=
 =?UTF-8?q?=ED=96=89=20=EA=B8=B0=EB=8A=A5=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/main/java/baseball/Application.java | 10 ++++++++++
 1 file changed, 10 insertions(+)
 create mode 100644 src/main/java/baseball/Application.java

diff --git a/src/main/java/baseball/Application.java b/src/main/java/baseball/Application.java
new file mode 100644
index 00000000..57aea45f
--- /dev/null
+++ b/src/main/java/baseball/Application.java
@@ -0,0 +1,10 @@
+package baseball;
+
+import baseball.game.Game;
+
+public class Application {
+    public static void main(String[] args) {
+        Game game = new Game();
+        game.startGame();
+    }
+}

From e235003b8fba8fdd1abc8d6db1916f598f228955 Mon Sep 17 00:00:00 2001
From: myeongheon <axm0219@naver.com>
Date: Tue, 7 May 2024 23:57:14 +0900
Subject: [PATCH 08/10] =?UTF-8?q?feat:=20NumberGenerator=20=ED=85=8C?=
 =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../baseball/domain/NumberGeneratorTest.java   | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)
 create mode 100644 src/test/java/baseball/domain/NumberGeneratorTest.java

diff --git a/src/test/java/baseball/domain/NumberGeneratorTest.java b/src/test/java/baseball/domain/NumberGeneratorTest.java
new file mode 100644
index 00000000..fd27dbba
--- /dev/null
+++ b/src/test/java/baseball/domain/NumberGeneratorTest.java
@@ -0,0 +1,18 @@
+package baseball.domain;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThat;
+
+class NumberGeneratorTest {
+
+    @Test
+    @DisplayName("testGenerateNumber() : 컴퓨터는 1-9까지 서로 다른 임의의 수를 3개를 선택한다.")
+    public void testGenerateNumber() {
+        String number = NumberGenerator.generateNumber();
+        assertThat(number).hasSize(3);
+        assertThat(number).matches("[1-9]+");
+        assertThat(number.chars().distinct().count()).isEqualTo(3);
+    }
+
+}
\ No newline at end of file

From 1f91c8822850b52b4a091fa3411b7df543c38d35 Mon Sep 17 00:00:00 2001
From: myeongheon <axm0219@naver.com>
Date: Tue, 7 May 2024 23:57:53 +0900
Subject: [PATCH 09/10] =?UTF-8?q?feat:=20InputValidatorTest=20=ED=85=8C?=
 =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../baseball/game/InputValidatorTest.java     | 33 +++++++++++++++++++
 1 file changed, 33 insertions(+)
 create mode 100644 src/test/java/baseball/game/InputValidatorTest.java

diff --git a/src/test/java/baseball/game/InputValidatorTest.java b/src/test/java/baseball/game/InputValidatorTest.java
new file mode 100644
index 00000000..dd8e30d9
--- /dev/null
+++ b/src/test/java/baseball/game/InputValidatorTest.java
@@ -0,0 +1,33 @@
+package baseball.game;
+
+import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThat;
+
+class InputValidatorTest {
+
+    @Test
+    public void testIsValidInput_Valid() {
+        assertThat(InputValidator.isValidInput("123")).isTrue();
+    }
+
+    @Test
+    public void testIsValidInput_InvalidLength() {
+        assertThat(InputValidator.isValidInput("12")).isFalse();
+    }
+
+    @Test
+    public void testIsValidInput_ContainsNonDigit() {
+        assertThat(InputValidator.isValidInput("1a3")).isFalse();
+    }
+
+    @Test
+    public void testIsValidInput_ContainsZero() {
+        assertThat(InputValidator.isValidInput("103")).isFalse();
+    }
+
+    @Test
+    public void testIsValidInput_DuplicateDigits() {
+        assertThat(InputValidator.isValidInput("112")).isFalse();
+    }
+
+}
\ No newline at end of file

From 6b73cc4dcc428cc787d745f346cdba11dbfe6eb2 Mon Sep 17 00:00:00 2001
From: myeongheon <axm0219@naver.com>
Date: Tue, 7 May 2024 23:58:16 +0900
Subject: [PATCH 10/10] =?UTF-8?q?feat:=20GameRulesTest=20=ED=85=8C?=
 =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../java/baseball/domain/GameRulesTest.java   | 62 +++++++++++++++++++
 1 file changed, 62 insertions(+)
 create mode 100644 src/test/java/baseball/domain/GameRulesTest.java

diff --git a/src/test/java/baseball/domain/GameRulesTest.java b/src/test/java/baseball/domain/GameRulesTest.java
new file mode 100644
index 00000000..87556a77
--- /dev/null
+++ b/src/test/java/baseball/domain/GameRulesTest.java
@@ -0,0 +1,62 @@
+package baseball.domain;
+
+import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThat;
+
+class GameRulesTest {
+
+    @Test
+    public void testGenerateHint_AllStrikes() {
+        GameRules rules = new GameRules();
+        String hint = rules.generateHint("123", "123");
+        assertThat(hint).isEqualTo("3스트라이크");
+    }
+
+    @Test
+    public void testGenerateHint_SomeBalls() {
+        GameRules rules = new GameRules();
+        String hint = rules.generateHint("123", "231");
+        assertThat(hint).isEqualTo("3볼");
+    }
+
+    @Test
+    public void testGenerateHint_Mixed() {
+        GameRules rules = new GameRules();
+        String hint = rules.generateHint("123", "132");
+        assertThat(hint).isEqualTo("2볼 1스트라이크");
+    }
+
+    @Test
+    public void testGenerateHint_Nothing() {
+        GameRules rules = new GameRules();
+        String hint = rules.generateHint("123", "456");
+        assertThat(hint).isEqualTo("낫싱");
+    }
+
+    @Test
+    public void testDecideRestart_ReturnsTrueFor1() {
+        GameRules rules = new GameRules();
+        Boolean result = rules.decideRestart("1");
+        assertThat(result).isTrue();
+    }
+
+    @Test
+    public void testDecideRestart_ReturnsFalseFor2() {
+        GameRules rules = new GameRules();
+        Boolean result = rules.decideRestart("2");
+        assertThat(result).isFalse();
+    }
+
+    @Test
+    public void testDecideRestart_ReturnsNullForOtherInputs() {
+        GameRules rules = new GameRules();
+        Boolean result = rules.decideRestart("3");
+        assertThat(result).isNull();
+
+        result = rules.decideRestart("hello");
+        assertThat(result).isNull();
+
+        result = rules.decideRestart("");
+        assertThat(result).isNull();
+    }
+}
\ No newline at end of file