diff --git a/README.md b/README.md
index bd90ef0247..a897fff0bb 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,41 @@
-# java-calculator-precourse
\ No newline at end of file
+# java-calculator-precourse
+
+## 기능 요구사항
+입력한 문자열에서 숫자를 추출하여 더하는 계산기를 구현한다.
+
+쉼표(,) 또는 콜론(:)을 구분자로 가지는 문자열을 전달하는 경우 구분자를 기준으로 분리한 각 숫자의 합을 반환한다.
+
+예: "" => 0, "1,2" => 3, "1,2,3" => 6, "1,2:3" => 6
+
+앞의 기본 구분자(쉼표, 콜론) 외에 커스텀 구분자를 지정할 수 있다.
+커스텀 구분자는 문자열 앞부분의 "//"와 "\n" 사이에 위치하는 문자를 커스텀 구분자로 사용한다.
+
+예를 들어 `"//;\n1;2;3"`과 같이 값을 입력할 경우 커스텀 구분자는 세미콜론(;)이며, 결과 값은 `6`이 반환되어야 한다.
+
+사용자가 잘못된 값을 입력할 경우 `IllegalArgumentException`을 발생시킨 후 애플리케이션은 종료되어야 한다.
+
+- 입력 : 구분자와 양수로 구성된 문자열
+- 출력 : 덧셈 결과
+
+### 라이브러리
+`camp.nextstep.edu.missionutils`에서 제공하는 Console API를 사용하여 구현해야 한다.
+사용자가 입력하는 값은 `camp.nextstep.edu.missionutils.Console`의 `readLine()`을 활용한다.
+## 기능 목록
+
+- [x] 문자열 입력받기
+ - [x] 입력 클래스 분리
+ - [x] 문자열 입력 문구 출력
+ - [x] 라이브러리를 이용한 문자열 입력
+- [x] 문자열 쉼표 및 세미콜론, 커스텀 구분자로 나누기
+ - [x] 문자열 분석 클래스 분리
+ - [x] 구분자 분리
+ - [x] 커스텀 구분자 분리
+ - [x] 입력값에서 쉼표 및 세미콜론 또는 커스텀 구분자만 사용되었는지 검증
+ - [x] 구분 인덱스 enum으로 관리
+ - [x] 커스텀 구분자 구문 삭제 로직 구현
+ - [x] 구분자를 이용하여 문자열을 숫자만 있는 배열로 추출
+- [x] 숫자 덧셈
+ - [x] 계산 클래스 분리
+ - [x] 구분자를 비교하여 숫자 덧셈 구현
+- [x] runner 구현
+- [x] 결과 반환
\ No newline at end of file
diff --git a/src/main/java/calculator/Application.java b/src/main/java/calculator/Application.java
index 573580fb40..fffe8b4b00 100644
--- a/src/main/java/calculator/Application.java
+++ b/src/main/java/calculator/Application.java
@@ -1,7 +1,12 @@
package calculator;
+import calculator.runner.CalculateRunner;
+
public class Application {
public static void main(String[] args) {
- // TODO: 프로그램 구현
+
+ CalculateRunner runner = CalculateRunner.getInstance();
+
+ runner.run();
}
}
diff --git a/src/main/java/calculator/calculation/Calculation.java b/src/main/java/calculator/calculation/Calculation.java
new file mode 100644
index 0000000000..b3eb4e99d1
--- /dev/null
+++ b/src/main/java/calculator/calculation/Calculation.java
@@ -0,0 +1,20 @@
+package calculator.calculation;
+
+public class Calculation {
+
+ private static final Calculation INSTANCE = new Calculation();
+
+ private Calculation() {}
+
+ public static Calculation getInstance() {
+ return INSTANCE;
+ }
+
+ public int calculate(String[] numbers) {
+ int sum = 0;
+ for(String number : numbers) {
+ sum += Integer.parseInt(number);
+ }
+ return sum;
+ }
+}
diff --git a/src/main/java/calculator/global/DefaultDelimiter.java b/src/main/java/calculator/global/DefaultDelimiter.java
new file mode 100644
index 0000000000..fcfb1f61ad
--- /dev/null
+++ b/src/main/java/calculator/global/DefaultDelimiter.java
@@ -0,0 +1,17 @@
+package calculator.global;
+
+public enum DefaultDelimiter {
+
+ COMMA(","),
+ COLON(":");
+
+ private final String key;
+
+ DefaultDelimiter(String key) {
+ this.key = key;
+ }
+
+ public String getKey() {
+ return key;
+ }
+}
diff --git a/src/main/java/calculator/global/DelimiterSyntaxIndex.java b/src/main/java/calculator/global/DelimiterSyntaxIndex.java
new file mode 100644
index 0000000000..e7e1fb436d
--- /dev/null
+++ b/src/main/java/calculator/global/DelimiterSyntaxIndex.java
@@ -0,0 +1,19 @@
+package calculator.global;
+
+public enum DelimiterSyntaxIndex {
+
+ DELIMITER_INDEX(2),
+ FIRST_END(3),
+ SECOND_END(4),
+ DELIMITER_AFTER(5);
+
+ private final int key;
+
+ DelimiterSyntaxIndex(int key) {
+ this.key = key;
+ }
+
+ public int getKey() {
+ return key;
+ }
+}
diff --git a/src/main/java/calculator/input/InputHandler.java b/src/main/java/calculator/input/InputHandler.java
new file mode 100644
index 0000000000..ca13ce7b23
--- /dev/null
+++ b/src/main/java/calculator/input/InputHandler.java
@@ -0,0 +1,23 @@
+package calculator.input;
+
+import camp.nextstep.edu.missionutils.Console;
+
+public class InputHandler {
+
+ private static final InputHandler INSTANCE = new InputHandler();
+
+ private InputHandler() {}
+
+ public static InputHandler getInstance() {
+ return INSTANCE;
+ }
+
+ public String getInput() {
+ System.out.println("문자열을 입력해주세요");
+ return Console.readLine();
+ }
+
+ public void closeInput() {
+ Console.close();
+ }
+}
diff --git a/src/main/java/calculator/parser/Delimiter.java b/src/main/java/calculator/parser/Delimiter.java
new file mode 100644
index 0000000000..4f036a9560
--- /dev/null
+++ b/src/main/java/calculator/parser/Delimiter.java
@@ -0,0 +1,46 @@
+package calculator.parser;
+
+import calculator.global.DefaultDelimiter;
+import calculator.global.DelimiterSyntaxIndex;
+
+public class Delimiter {
+
+ private static final Delimiter INSTANCE = new Delimiter();
+
+ private Delimiter() {}
+
+ public static Delimiter getInstance() {
+ return INSTANCE;
+ }
+
+ public String extractDelimiter(String input) {
+ if (validateStartsWith(input) && validateEndsWith(input)) {
+ String rawDelimiter = String.valueOf(input.charAt(DelimiterSyntaxIndex.DELIMITER_INDEX.getKey()));
+
+ return rawDelimiter.matches("[.\\^$|?*+(){}\\[\\]\\\\]") ? "\\" + rawDelimiter : rawDelimiter;
+ }
+ return "";
+ }
+
+ public String removeDelimiterSyntax(String input) {
+ if (validateStartsWith(input) && validateEndsWith(input)) {
+ return input.substring(DelimiterSyntaxIndex.DELIMITER_AFTER.getKey());
+ }
+ return input;
+ }
+
+ public boolean validateHasDefaultOrCustomDelimiter(String input, String customDelimiter) {
+ String delimiters = DefaultDelimiter.COLON.getKey() + DefaultDelimiter.COMMA.getKey() + customDelimiter;
+ String defaultSyntax = removeDelimiterSyntax(input);
+ return defaultSyntax.matches("[0-9" + delimiters + "]*");
+ }
+
+ private boolean validateStartsWith(String input) {
+ return input.startsWith("//");
+ }
+
+ private boolean validateEndsWith(String input) {
+ return input.charAt(DelimiterSyntaxIndex.FIRST_END.getKey()) == '\\'
+ && input.charAt(DelimiterSyntaxIndex.SECOND_END.getKey()) == 'n';
+ }
+}
diff --git a/src/main/java/calculator/parser/InputParser.java b/src/main/java/calculator/parser/InputParser.java
new file mode 100644
index 0000000000..b37066f999
--- /dev/null
+++ b/src/main/java/calculator/parser/InputParser.java
@@ -0,0 +1,37 @@
+package calculator.parser;
+
+import calculator.global.DefaultDelimiter;
+
+import java.util.Arrays;
+
+public class InputParser {
+
+ private static final InputParser INSTANCE = new InputParser();
+ private final Delimiter delimiter;
+
+ private InputParser() {
+ this.delimiter = Delimiter.getInstance();
+ }
+
+ public static InputParser getInstance() {
+ return INSTANCE;
+ }
+
+ public String[] parse(String input) {
+ String customDelimiter = delimiter.extractDelimiter(input);
+ if (!delimiter.validateHasDefaultOrCustomDelimiter(input, customDelimiter)) {
+ throw new IllegalArgumentException("허용되지 않는 입력문입니다.");
+ }
+ String defaultSyntax = delimiter.removeDelimiterSyntax(input);
+ return extractNumbersFromDefaultSyntax(defaultSyntax, customDelimiter);
+ }
+
+ private String[] extractNumbersFromDefaultSyntax(String defaultSyntax, String customDelimiter) {
+ String delimiters = DefaultDelimiter.COMMA.getKey() + DefaultDelimiter.COLON.getKey() + customDelimiter;
+ String[] numbers = defaultSyntax.split("[" + delimiters + "]");
+
+ return Arrays.stream(numbers)
+ .filter(num -> !num.isEmpty())
+ .toArray(String[]::new);
+ }
+}
diff --git a/src/main/java/calculator/runner/CalculateRunner.java b/src/main/java/calculator/runner/CalculateRunner.java
new file mode 100644
index 0000000000..81bf741635
--- /dev/null
+++ b/src/main/java/calculator/runner/CalculateRunner.java
@@ -0,0 +1,34 @@
+package calculator.runner;
+
+import calculator.calculation.Calculation;
+import calculator.input.InputHandler;
+import calculator.parser.InputParser;
+
+public class CalculateRunner {
+
+ private static final CalculateRunner INSTANCE = new CalculateRunner();
+
+ public static CalculateRunner getInstance() {
+ return INSTANCE;
+ }
+
+ private final Calculation calculator;
+ private final InputParser parser;
+ private final InputHandler inputHandler;
+
+ private CalculateRunner() {
+ this.calculator = Calculation.getInstance();
+ this.parser = InputParser.getInstance();
+ this.inputHandler = InputHandler.getInstance();
+ }
+
+ public void run() {
+ String input = inputHandler.getInput();
+ String[] parseResult = parser.parse(input);
+ int result = calculator.calculate(parseResult);
+
+ System.out.println("Result: " + result);
+
+ inputHandler.closeInput();
+ }
+}
diff --git a/src/test/java/calculator/calculation/CalculationTest.java b/src/test/java/calculator/calculation/CalculationTest.java
new file mode 100644
index 0000000000..af4b02d1a1
--- /dev/null
+++ b/src/test/java/calculator/calculation/CalculationTest.java
@@ -0,0 +1,48 @@
+package calculator.calculation;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class CalculationTest {
+
+ private final Calculation calculator = Calculation.getInstance();
+
+ @Test
+ @DisplayName("정상적인 숫자 배열로 계산")
+ void testCalculateWithValidNumbers() {
+ // Given
+ String[] numbers = {"1", "2", "3"};
+
+ // When
+ int result = calculator.calculate(numbers);
+
+ // Then
+ assertEquals(6, result);
+ }
+
+ @Test
+ @DisplayName("빈 배열 입력 시 0 반환")
+ void testCalculateWithEmptyArray() {
+ // Given
+ String[] numbers = {};
+
+ // When
+ int result = calculator.calculate(numbers);
+
+ // Then
+ assertEquals(0, result);
+ }
+
+ @Test
+ @DisplayName("숫자가 아닌 값이 포함되었을 때 예외 발생")
+ void testCalculateWithInvalidNumber() {
+ // Given
+ String[] numbers = {"1", "a", "3"};
+
+ // When & Then
+ assertThrows(IllegalArgumentException.class, () -> calculator.calculate(numbers));
+ }
+}
diff --git a/src/test/java/calculator/delimiter/DelimiterTest.java b/src/test/java/calculator/delimiter/DelimiterTest.java
new file mode 100644
index 0000000000..86d166b75f
--- /dev/null
+++ b/src/test/java/calculator/delimiter/DelimiterTest.java
@@ -0,0 +1,60 @@
+package calculator.delimiter;
+
+import calculator.parser.Delimiter;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class DelimiterTest {
+
+ private final Delimiter delimiter = Delimiter.getInstance();
+
+ @Test
+ @DisplayName("커스텀 구분자를 올바르게 추출")
+ void testExtractDelimiterWithCustomSyntax() {
+ // Given
+ String input = "//;\\n1;2;3";
+
+ // When
+ String result = delimiter.extractDelimiter(input);
+
+ // Then
+ assertEquals(";", result);
+ }
+
+ @Test
+ @DisplayName("구분자 구문 제거 테스트")
+ void testRemoveDelimiterSyntax() {
+ // Given
+ String input = "//;\\n1;2;3";
+
+ // When
+ String result = delimiter.removeDelimiterSyntax(input);
+
+ // Then
+ assertEquals("1;2;3", result);
+ }
+
+ @Test
+ @DisplayName("기본 또는 커스텀 구분자 유효성 테스트")
+ void testValidateHasDefaultOrCustomDelimiter() {
+ // Given
+ String input = "//;\\n1;2;3";
+
+ // When & Then
+ assertTrue(delimiter.validateHasDefaultOrCustomDelimiter(input, ";"));
+ }
+
+ @Test
+ @DisplayName("잘못된 구분자 입력 시 실패")
+ void testValidateInvalidDelimiter() {
+ // Given
+ String input = "//;\\n1,2;3";
+
+ // When & Then
+ assertFalse(delimiter.validateHasDefaultOrCustomDelimiter(input, "|"));
+ }
+}
diff --git a/src/test/java/calculator/input/InputTest.java b/src/test/java/calculator/input/InputTest.java
new file mode 100644
index 0000000000..573e4a6db0
--- /dev/null
+++ b/src/test/java/calculator/input/InputTest.java
@@ -0,0 +1,31 @@
+package calculator.input;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayInputStream;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+
+public class InputTest {
+
+ private final InputHandler inputHandler = InputHandler.getInstance();
+
+ @Test
+ @DisplayName("사용자 입력을 정상적으로 받는지 테스트")
+ void testGetInput() {
+ // Given
+ String simulatedInput = "1,2,3";
+ System.setIn(new ByteArrayInputStream(simulatedInput.getBytes()));
+
+ // When & Then
+ assertDoesNotThrow(inputHandler::getInput);
+ }
+
+ @Test
+ @DisplayName("입력 스트림을 정상적으로 닫는지 테스트")
+ void testCloseInput() {
+ // Given & When & Then
+ assertDoesNotThrow(inputHandler::closeInput);
+ }
+}
diff --git a/src/test/java/calculator/parser/ParserTest.java b/src/test/java/calculator/parser/ParserTest.java
new file mode 100644
index 0000000000..0d9dc87e22
--- /dev/null
+++ b/src/test/java/calculator/parser/ParserTest.java
@@ -0,0 +1,48 @@
+package calculator.parser;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class ParserTest {
+
+ private final InputParser parser = InputParser.getInstance();
+
+ @Test
+ @DisplayName("커스텀 구분자로 입력을 파싱")
+ void testParseWithCustomDelimiter() {
+ // Given
+ String input = "//;\\n1;2;3";
+
+ // When
+ String[] result = parser.parse(input);
+
+ // Then
+ assertArrayEquals(new String[]{"1", "2", "3"}, result);
+ }
+
+ @Test
+ @DisplayName("기본 구분자로 입력을 파싱")
+ void testParseWithDefaultDelimiter() {
+ // Given
+ String input = "1,2:3";
+
+ // When
+ String[] result = parser.parse(input);
+
+ // Then
+ assertArrayEquals(new String[]{"1", "2", "3"}, result);
+ }
+
+ @Test
+ @DisplayName("잘못된 구분자 구문 입력 시 예외 발생")
+ void testParseWithInvalidDelimiter() {
+ // Given
+ String input = "///;\\n1,2:3";
+
+ // When & Then
+ assertThrows(IllegalArgumentException.class, () -> parser.parse(input));
+ }
+}
diff --git a/src/test/java/calculator/runner/RunnerTest.java b/src/test/java/calculator/runner/RunnerTest.java
new file mode 100644
index 0000000000..1b459139ea
--- /dev/null
+++ b/src/test/java/calculator/runner/RunnerTest.java
@@ -0,0 +1,23 @@
+package calculator.runner;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayInputStream;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+
+public class RunnerTest {
+
+ @Test
+ @DisplayName("CalculateRunner 실행 중 예외 발생 없이 실행")
+ void testRunnerExecution() {
+ // Given
+ CalculateRunner runner = CalculateRunner.getInstance();
+ String simulatedInput = "1,2:3";
+ System.setIn(new ByteArrayInputStream(simulatedInput.getBytes()));
+
+ // When & Then
+ assertDoesNotThrow(runner::run);
+ }
+}