diff --git a/README.md b/README.md
index 8d7e8aee..d4befb08 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,68 @@
-# java-baseball-precourse
\ No newline at end of file
+# 숫자 야구 게임 설계 문서
+
+이 문서는 숫자 야구 게임의 핵심 로직과 상태 패턴(State Pattern)을 이용한 흐름 제어 설계를 포함합니다.
+
+---
+
+## 구현 범위
+
+이번 과제에서 최종적으로 구현할 항목은 아래와 같습니다.
+
+- 1~9 사이의 서로 다른 3자리 컴퓨터 숫자 생성
+- 사용자 숫자 입력 처리 및 입력값 검증(길이, 숫자 여부, 범위, 중복)
+- 잘못된 입력 시 `[ERROR]` 메시지 출력 후 재입력
+- 스트라이크/볼/낫싱 계산과 결과 출력
+- 3스트라이크 달성 시 게임 종료 메시지 출력
+- 게임 종료 후 재시작(1) / 종료(2) 분기 처리
+- 상태 패턴(`ProgressState`, `GameOverState`, `EndState`) 기반 흐름 제어
+- 도메인 로직 단위 테스트(`BaseballNumber`, `NumberGenerator`, `GameResult`)
+
+---
+
+## 1. 도메인 모델
+
+### BaseballNumbers
+사용자 또는 컴퓨터의 숫자를 관리하는 객체입니다.
+- **검증**: 1~9 사이의 서로 다른 3자리 숫자인지 검증합니다.
+- **비교**: 상대방의 `BaseballNumbers`와 비교하여 스트라이크와 볼의 개수를 계산합니다.
+
+### GameResult
+계산된 스트라이크/볼 결과를 처리하는 객체입니다.
+- **결과 저장**: 계산된 스트라이크와 볼의 개수를 보관합니다.
+- **메시지 생성**: 결과에 따른 출력 메시지(예: `1볼 2스트라이크`, `낫싱`)를 생성합니다.
+- **판단**: 3 스트라이크인지 여부를 확인하여 게임 종료 조건을 체크합니다.
+
+---
+
+## 2. 컨트롤러 및 실행 (Game Controller)
+
+### Game
+전체적인 게임 인스턴스와 흐름을 관리합니다.
+- **상태 관리**: 현재의 `GameState`를 유지합니다.
+- **게임 루프**: 현재 상태가 종료 상태(`EndState`)가 될 때까지 실행을 반복합니다.
+- **유연성**: 상태 패턴을 통해 게임 종료 후 새로운 게임을 시작하거나 완전히 종료하는 로직을 매끄럽게 연결합니다.
+
+---
+
+## 3. 상태 인터페이스 및 구현 (State Pattern)
+
+상태 패턴을 활용하여 각 상황에 맞는 로직을 캡슐화하고 흐름을 제어합니다.
+
+
+
+| 클래스명 | 역할 및 주요 기능 | 다음 상태 전이 (Next State) |
+| :--- | :--- | :--- |
+| **ProgressState** | - "숫자를 입력해주세요" 프롬프트 출력
- 사용자 입력을 받아 `BaseballNumbers`와 비교 후 결과 출력 | - 정답이 아니면: `this` (유지)
- 정답이면: `GameOverState` 반환 |
+| **GameOverState** | - "3개의 숫자를 모두 맞히셨습니다! 게임 끝" 및 재시작 안내 출력
- 사용자 입력(1: 재시작, 2: 종료) 대기 | - `1` 입력 시: 새로운 정답을 가진 `ProgressState`
- `2` 입력 시: `EndState` |
+| **EndState** | - 게임 루프를 종료해야 함을 `Game`에게 알림 | - 없음 (시스템 종료) |
+
+---
+
+## 4. 게임 실행 흐름 (Game Flow)
+
+1. **초기화**: `Game` 객체가 생성될 때 컴퓨터의 숫자를 생성하고 `ProgressState`로 시작합니다.
+2. **진행**: 사용자가 정답을 맞힐 때까지 `ProgressState` 내에서 입력을 반복합니다.
+3. **완료**: 3 스트라이크 달성 시 `GameOverState`로 전환되어 축하 메시지를 출력합니다.
+4. **분기**:
+ - 사용자가 `1`을 입력하면 새로운 숫자를 생성하여 다시 `ProgressState`로 돌아갑니다.
+ - 사용자가 `2`를 입력하면 `EndState`로 전환되어 루프가 종료됩니다.
diff --git a/src/main/java/Main.java b/src/main/java/Main.java
new file mode 100644
index 00000000..e3287045
--- /dev/null
+++ b/src/main/java/Main.java
@@ -0,0 +1,8 @@
+import controller.GameController;
+
+public class Main{
+ public static void main(String[] args) {
+ GameController gameController = new GameController();
+ gameController.run();
+ }
+}
diff --git a/src/main/java/controller/GameController.java b/src/main/java/controller/GameController.java
new file mode 100644
index 00000000..14dcfed5
--- /dev/null
+++ b/src/main/java/controller/GameController.java
@@ -0,0 +1,31 @@
+package controller;
+
+import model.BaseballNumber;
+import model.Game;
+import model.NumberGenerator;
+import state.GameState;
+import state.ProgressState;
+
+public class GameController {
+ public Game game;
+ public NumberGenerator numberGenerator;
+
+ public void run(){
+ initialize();
+ play();
+ }
+
+ public void initialize(){
+ this.numberGenerator = new NumberGenerator();
+ BaseballNumber baseballNumber = numberGenerator.generate();
+ GameState initialGameState = new ProgressState(baseballNumber);
+
+ this.game = new Game(initialGameState);
+ }
+
+ public void play(){
+ while(game.isRunning()){
+ game.update();
+ }
+ }
+}
diff --git a/src/main/java/model/BaseballNumber.java b/src/main/java/model/BaseballNumber.java
new file mode 100644
index 00000000..0c7ce006
--- /dev/null
+++ b/src/main/java/model/BaseballNumber.java
@@ -0,0 +1,92 @@
+package model;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class BaseballNumber {
+ private final List numbers;
+
+ public BaseballNumber(List numbers){
+ this.numbers = new ArrayList<>(numbers);
+ validateNumbers();
+ }
+
+ public BaseballNumber(String input){
+ this.numbers = parse(input);
+ validateNumbers();
+ }
+
+ public boolean isValid(){
+ if(numbers.size() != 3) return false;
+ if(!isRangeValid()) return false;
+ if(hasDuplicate()) return false;
+ return true;
+ }
+
+ public List getNumbers(){
+ return new ArrayList<>(numbers);
+ }
+
+ public GameResult compare(BaseballNumber target){
+ int strike = countStrike(target);
+ int ball = countBall(target);
+ return new GameResult(strike, ball);
+ }
+
+ private int countStrike(BaseballNumber target){
+ List targetNumbers = target.getNumbers();
+ int strike = 0;
+ for (int i = 0; i < numbers.size(); i++) {
+ if(numbers.get(i).equals(targetNumbers.get(i))) strike++;
+ }
+ return strike;
+ }
+
+ private int countBall(BaseballNumber target){
+ List targetNumbers = target.getNumbers();
+ int ball = 0;
+ for (int i = 0; i < numbers.size(); i++) {
+ for (int j = 0; j < targetNumbers.size(); j++) {
+ if(i == j) continue;
+ if(numbers.get(i).equals(targetNumbers.get(j))) ball++;
+ }
+ }
+ return ball;
+ }
+
+ private List parse(String input){
+ if(input == null) throw new IllegalArgumentException("입력값이 비어 있습니다.");
+ if(input.length() != 3) throw new IllegalArgumentException("3자리 숫자를 입력해야 합니다.");
+
+ List parsed = new ArrayList<>();
+ for (int i = 0; i < input.length(); i++) {
+ char ch = input.charAt(i);
+ if(!Character.isDigit(ch)) throw new IllegalArgumentException("숫자만 입력해야 합니다.");
+ parsed.add(ch - '0');
+ }
+ return parsed;
+ }
+
+ private void validateNumbers(){
+ if(isValid()) return;
+ throw new IllegalArgumentException("1~9의 서로 다른 3자리 숫자를 입력해야 합니다.");
+ }
+
+ private boolean isRangeValid(){
+ for (int i = 0; i < numbers.size(); i++) {
+ int number = numbers.get(i);
+ if(number < 1 || number > 9) return false;
+ }
+ return true;
+ }
+
+ private boolean hasDuplicate(){
+ for (int i = 0; i < numbers.size(); i++) {
+ for (int j = i + 1; j < numbers.size(); j++) {
+ if(numbers.get(i).equals(numbers.get(j))) return true;
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/src/main/java/model/Game.java b/src/main/java/model/Game.java
new file mode 100644
index 00000000..8b0668df
--- /dev/null
+++ b/src/main/java/model/Game.java
@@ -0,0 +1,28 @@
+package model;
+
+import state.GameState;
+
+public class Game {
+ private GameState state;
+ private boolean running = true;
+
+ public Game(GameState initialState) {
+ this.state = initialState;
+ }
+
+ public void changeState(GameState state) {
+ this.state = state;
+ }
+
+ public void update() {
+ state.handle(this);
+ }
+
+ public void stop() {
+ this.running = false;
+ }
+
+ public boolean isRunning() {
+ return running;
+ }
+}
diff --git a/src/main/java/model/GameResult.java b/src/main/java/model/GameResult.java
new file mode 100644
index 00000000..67cb7123
--- /dev/null
+++ b/src/main/java/model/GameResult.java
@@ -0,0 +1,30 @@
+package model;
+
+public class GameResult {
+ private final int strike;
+ private final int ball;
+
+ public GameResult(int strike, int ball){
+ this.strike = strike;
+ this.ball = ball;
+ }
+
+ public int getStrike(){
+ return strike;
+ }
+
+ public int getBall(){
+ return ball;
+ }
+
+ public boolean isThreeStrike(){
+ return strike == 3;
+ }
+
+ public String getMessage(){
+ if(strike == 0 && ball == 0) return "낫싱";
+ if(strike == 0) return ball + "볼";
+ if(ball == 0) return strike + "스트라이크";
+ return strike + "스트라이크 " + ball + "볼";
+ }
+}
diff --git a/src/main/java/model/NumberGenerator.java b/src/main/java/model/NumberGenerator.java
new file mode 100644
index 00000000..d79b9200
--- /dev/null
+++ b/src/main/java/model/NumberGenerator.java
@@ -0,0 +1,22 @@
+package model;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class NumberGenerator {
+
+ public BaseballNumber generate(){
+ List nums = new ArrayList<>();
+ for (int i = 1; i <= 9; i++) {
+ nums.add(i);
+ }
+ Collections.shuffle(nums);
+
+ List pick = new ArrayList<>();
+ for (int i = 0; i < 3; i++) {
+ pick.add(nums.get(i));
+ }
+ return new BaseballNumber(pick);
+ }
+}
diff --git a/src/main/java/state/EndState.java b/src/main/java/state/EndState.java
new file mode 100644
index 00000000..5c024f19
--- /dev/null
+++ b/src/main/java/state/EndState.java
@@ -0,0 +1,10 @@
+package state;
+
+import model.Game;
+
+public class EndState implements GameState{
+ @Override
+ public void handle(Game game){
+ game.stop();
+ }
+}
diff --git a/src/main/java/state/GameOverState.java b/src/main/java/state/GameOverState.java
new file mode 100644
index 00000000..719c6050
--- /dev/null
+++ b/src/main/java/state/GameOverState.java
@@ -0,0 +1,37 @@
+package state;
+
+import model.BaseballNumber;
+import model.Game;
+import model.NumberGenerator;
+import view.Inputview;
+import view.OutputView;
+
+public class GameOverState implements GameState{
+ private final Inputview inputview = new Inputview();
+ private final OutputView outputView = new OutputView();
+
+ @Override
+ public void handle(Game game){
+ outputView.printRestartGuide();
+ String input = inputview.readRestart();
+ changeState(input, game);
+ }
+
+ private void changeState(String input, Game game){
+ if("1".equals(input)) {
+ restart(game);
+ return;
+ }
+ if("2".equals(input)) {
+ game.changeState(new EndState());
+ return;
+ }
+ outputView.printError("1 또는 2를 입력해야 합니다.");
+ }
+
+ private void restart(Game game){
+ NumberGenerator numberGenerator = new NumberGenerator();
+ BaseballNumber baseballNumber = numberGenerator.generate();
+ game.changeState(new ProgressState(baseballNumber));
+ }
+}
diff --git a/src/main/java/state/GameState.java b/src/main/java/state/GameState.java
new file mode 100644
index 00000000..fe1bc76c
--- /dev/null
+++ b/src/main/java/state/GameState.java
@@ -0,0 +1,7 @@
+package state;
+
+import model.Game;
+
+public interface GameState {
+ void handle(Game game);
+}
diff --git a/src/main/java/state/ProgressState.java b/src/main/java/state/ProgressState.java
new file mode 100644
index 00000000..11ea03c3
--- /dev/null
+++ b/src/main/java/state/ProgressState.java
@@ -0,0 +1,39 @@
+package state;
+
+import model.BaseballNumber;
+import model.Game;
+import model.GameResult;
+import view.Inputview;
+import view.OutputView;
+
+public class ProgressState implements GameState{
+ private final BaseballNumber answer;
+ private final Inputview inputview;
+ private final OutputView outputView;
+
+ public ProgressState(BaseballNumber baseballNumber){
+ this.answer = baseballNumber;
+ this.inputview = new Inputview();
+ this.outputView = new OutputView();
+ }
+
+ @Override
+ public void handle(Game game){
+ outputView.printInputGuide();
+ String input = inputview.readNumber();
+ playOneTurn(input, game);
+ }
+
+ private void playOneTurn(String input, Game game){
+ try {
+ BaseballNumber userNumber = new BaseballNumber(input);
+ GameResult gameResult = answer.compare(userNumber);
+ outputView.printResult(gameResult.getMessage());
+ if(!gameResult.isThreeStrike()) return;
+ outputView.printGameEnd();
+ game.changeState(new GameOverState());
+ } catch (IllegalArgumentException e){
+ outputView.printError(e.getMessage());
+ }
+ }
+}
diff --git a/src/main/java/view/Inputview.java b/src/main/java/view/Inputview.java
new file mode 100644
index 00000000..b0c71027
--- /dev/null
+++ b/src/main/java/view/Inputview.java
@@ -0,0 +1,15 @@
+package view;
+
+import java.util.Scanner;
+
+public class Inputview {
+ private final Scanner scanner = new Scanner(System.in);
+
+ public String readNumber() {
+ return scanner.nextLine();
+ }
+
+ public String readRestart() {
+ return scanner.nextLine();
+ }
+}
diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java
new file mode 100644
index 00000000..1e4fc650
--- /dev/null
+++ b/src/main/java/view/OutputView.java
@@ -0,0 +1,23 @@
+package view;
+
+public class OutputView {
+ public void printInputGuide() {
+ System.out.print("숫자를 입력해주세요 : ");
+ }
+
+ public void printResult(String msg) {
+ System.out.println(msg);
+ }
+
+ public void printGameEnd() {
+ System.out.println("3개의 숫자를 모두 맞히셨습니다! 게임 시마이");
+ }
+
+ public void printRestartGuide() {
+ System.out.println("게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요.");
+ }
+
+ public void printError(String msg) {
+ System.out.println("[ERROR] " + msg);
+ }
+}
diff --git a/src/test/java/model/BaseballNumberTest.java b/src/test/java/model/BaseballNumberTest.java
new file mode 100644
index 00000000..aa4cfbac
--- /dev/null
+++ b/src/test/java/model/BaseballNumberTest.java
@@ -0,0 +1,55 @@
+package model;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class BaseballNumberTest {
+
+ @Test
+ void 문자열로_숫자야구_숫자를_생성한다() {
+ BaseballNumber baseballNumber = new BaseballNumber("123");
+
+ assertThat(baseballNumber.getNumbers()).containsExactly(1, 2, 3);
+ }
+
+ @Test
+ void 길이가_3이_아니면_예외가_발생한다() {
+ assertThatThrownBy(() -> new BaseballNumber("12"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("3자리");
+ }
+
+ @Test
+ void 숫자가_아닌_문자가_포함되면_예외가_발생한다() {
+ assertThatThrownBy(() -> new BaseballNumber("12a"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("숫자만");
+ }
+
+ @Test
+ void 범위를_벗어난_숫자가_포함되면_예외가_발생한다() {
+ assertThatThrownBy(() -> new BaseballNumber("120"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("1~9");
+ }
+
+ @Test
+ void 중복_숫자가_포함되면_예외가_발생한다() {
+ assertThatThrownBy(() -> new BaseballNumber("112"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("서로 다른");
+ }
+
+ @Test
+ void 비교_결과로_스트라이크와_볼을_계산한다() {
+ BaseballNumber answer = new BaseballNumber("425");
+ BaseballNumber user = new BaseballNumber("456");
+
+ GameResult gameResult = answer.compare(user);
+
+ assertThat(gameResult.getStrike()).isEqualTo(1);
+ assertThat(gameResult.getBall()).isEqualTo(1);
+ }
+}
diff --git a/src/test/java/model/NumberGeneratorTest.java b/src/test/java/model/NumberGeneratorTest.java
new file mode 100644
index 00000000..97ece531
--- /dev/null
+++ b/src/test/java/model/NumberGeneratorTest.java
@@ -0,0 +1,39 @@
+package model;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class NumberGeneratorTest {
+
+ @Test
+ void 생성된_숫자는_3자리다() {
+ NumberGenerator numberGenerator = new NumberGenerator();
+
+ BaseballNumber baseballNumber = numberGenerator.generate();
+
+ assertThat(baseballNumber.getNumbers()).hasSize(3);
+ }
+
+ @Test
+ void 생성된_숫자는_1부터_9_사이이고_중복이_없다() {
+ NumberGenerator numberGenerator = new NumberGenerator();
+
+ for (int i = 0; i < 100; i++) {
+ List numbers = numberGenerator.generate().getNumbers();
+ assertThat(numbers).hasSize(3);
+ assertThat(numbers).doesNotHaveDuplicates();
+ assertThat(isRange(numbers)).isTrue();
+ }
+ }
+
+ private boolean isRange(List numbers) {
+ for (int i = 0; i < numbers.size(); i++) {
+ int number = numbers.get(i);
+ if (number < 1 || number > 9) return false;
+ }
+ return true;
+ }
+}