From a36f659980794cc10af7803782b8593b59faf29b Mon Sep 17 00:00:00 2001 From: syg0629 Date: Mon, 20 Oct 2025 23:25:48 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=AC=B8=EC=9E=90=EC=97=B4=20=EB=8D=A7?= =?UTF-8?q?=EC=85=88=20=EA=B3=84=EC=82=B0=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 33 ++++++++++- __tests__/ApplicationTest.js | 105 ++++++++++++++++++++++++++++++++++- src/App.js | 93 ++++++++++++++++++++++++++++++- 3 files changed, 228 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 13420b29..3bd783d6 100644 --- a/README.md +++ b/README.md @@ -1 +1,32 @@ -# javascript-calculator-precourse \ No newline at end of file +# javascript-calculator-precourse + +## 문자열 덧셈 계산기 + +### 구현할 기능 목록 + +1. 입력 기능 작성 +2. 아래 해당되는 구분자로 문자열을 분리하는 함수 생성. ""일 경우 0을 반환 + - 기본 구분자(쉼표, 콜론) + - 커스텀 구분자("//"와 "\n" 사이에 위치하는 문자) +3. 구분자로 분리된 문자열을 숫자로 변환하여 더하는 함수 생성 +4. 입력 형식에 맞지 않을 경우 "[ERROR]"로 Error 처리 + - 유효하지 않은 구분자 + - 쉼표, 콜론, "//"와 "\n" 사이에 위치하는 문자가 아닌 경우 + - 음수 값 +5. 발견된 추가 케이스 구현 + - 기본 구분자와 커스텀 구분자가 혼용되어 있는 경우 + - 공백을 포함한 경우 + - 단일 숫자인 경우 + - 연속된 구분자일 경우 + - 잘못된 커스텀 구분자 형식 + - ERROR + - 숫자가 아닌 문자 포함 + - 숫자가 없는 커스텀 구분자 형식 + - 구분자 없음 + +### 발생 문제 정리 + +- 기본 구분자 이후 커스텀 구분자를 찾게 했는데, 내 코드에선 기본 구분자와 커스텀 구분자가 혼용되어 있는 경우 기본 구분자에서 바로 return을 하기에 커스텀 구분자를 확인하지 못함. + - 커스텀 구분자인지 먼저 확인 후 없으면 기본 구문자 확인으로 변경 +- 내가 작성한 테스트 입력들이 `\n`(actual newline)과 `\\n`(backslash + n 문자열) 두 가지 형태여서 오류가 섞여서 발생 + - `/^\/\/(.+?)(?:\\n|\n)/`을 사용하여 두 케이스 모두 대응으로 변경 \ No newline at end of file diff --git a/__tests__/ApplicationTest.js b/__tests__/ApplicationTest.js index 7c6962dd..695bd0cf 100644 --- a/__tests__/ApplicationTest.js +++ b/__tests__/ApplicationTest.js @@ -32,7 +32,7 @@ describe("문자열 계산기", () => { }); }); - test("예외 테스트", async () => { + test("예외 테스트 - 음수", async () => { const inputs = ["-1,2,3"]; mockQuestions(inputs); @@ -40,4 +40,107 @@ describe("문자열 계산기", () => { await expect(app.run()).rejects.toThrow("[ERROR]"); }); + + test("기본 구분자와 커스텀 구분자를 혼용하여 사용", async () => { + const inputs = ["//;\n1,2;3"]; + mockQuestions(inputs); + + const logSpy = getLogSpy(); + const outputs = ["결과 : 6"]; + + const app = new App(); + await app.run(); + + outputs.forEach((output) => { + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(output)); + }); + }); + + test("공백 포함", async () => { + const inputs = [",2"]; + mockQuestions(inputs); + + const logSpy = getLogSpy(); + const outputs = ["결과 : 2"]; + + const app = new App(); + await app.run(); + + outputs.forEach((output) => { + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(output)); + }); + }); + + test("공백 포함2", async () => { + const inputs = ["1,2,"]; + mockQuestions(inputs); + + const logSpy = getLogSpy(); + const outputs = ["결과 : 3"]; + + const app = new App(); + await app.run(); + + outputs.forEach((output) => { + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(output)); + }); + }); + + test("단일 숫자", async () => { + const inputs = ["1"]; + mockQuestions(inputs); + 3; + const logSpy = getLogSpy(); + const outputs = ["결과 : 1"]; + + const app = new App(); + await app.run(); + + outputs.forEach((output) => { + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(output)); + }); + }); + + test("연속된 구분자", async () => { + const inputs = ["1,,3"]; + mockQuestions(inputs); + + const logSpy = getLogSpy(); + const outputs = ["결과 : 4"]; + + const app = new App(); + await app.run(); + + outputs.forEach((output) => { + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(output)); + }); + }); + + test("예외테스트 - 잘못된 커스텀 구분자 형식 - 구분자 없음", async () => { + const inputs = ["//\n1,2,3"]; + mockQuestions(inputs); + const app = new App(); + await expect(app.run()).rejects.toThrow("[ERROR]"); + }); + + test("예외테스트 - 잘못된 커스텀 구분자 형식 - 숫자 없음", async () => { + const inputs = ["//;\n"]; + mockQuestions(inputs); + const app = new App(); + await expect(app.run()).rejects.toThrow("[ERROR]"); + }); + + test("예외테스트 - 숫자가 아닌 문자 포함", async () => { + const inputs = ["1,2,abc"]; + mockQuestions(inputs); + const app = new App(); + await expect(app.run()).rejects.toThrow("[ERROR]"); + }); + + test("예외테스트 - 유효하지 않은 구분자", async () => { + const inputs = ["1@3"]; + mockQuestions(inputs); + const app = new App(); + await expect(app.run()).rejects.toThrow("[ERROR]"); + }); }); diff --git a/src/App.js b/src/App.js index 091aa0a5..16018791 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,96 @@ +import { MissionUtils } from "@woowacourse/mission-utils"; + class App { - async run() {} + static DEFAULT_DELIMITERS = /,|:/; + static CUSTOM_DELIMITER_PATTERN = /^\/\/(.+?)(?:\\n|\n)/; + static ERROR_MESSAGES = { + INVALID_DELIMITER: "[ERROR] 유효하지 않은 구분자가 포함되어 있습니다.", + INVALID_NUMBER: "[ERROR] 유효하지 않은 숫자가 포함되어 있습니다.", + }; + + async run() { + try { + const input = await this.getUserInput(); + const result = this.getDelimiters(input); + this.printResult(result); + } catch (error) { + this.printError(error); + throw error; + } + } + + async getUserInput() { + return await MissionUtils.Console.readLineAsync( + "덧셈할 문자열을 입력해 주세요.\n" + ); + } + + // 쉼표, 콜론, 커스텀 구분자로 문자열을 분리하는 함수 + getDelimiters(str) { + // 공백 문자열 처리 + if (str.trim() === "") return 0; + + if (str.startsWith("//")) { + // 커스텀 구분자 형식 확인 + const customDelimiterMatch = str.match(App.CUSTOM_DELIMITER_PATTERN); + + if (!customDelimiterMatch) { + throw new Error(App.ERROR_MESSAGES.INVALID_DELIMITER); + } + + const customDelimiter = customDelimiterMatch[1]; + let numbersPart = str.slice(customDelimiterMatch[0].length); + + // 숫자 부분이 비어있으면 에러 + if (numbersPart.trim() === "") { + throw new Error(App.ERROR_MESSAGES.INVALID_DELIMITER); + } + + // 모든 구분자를 쉼표로 통일 + numbersPart = numbersPart.replaceAll(customDelimiter, ","); + numbersPart = numbersPart.replaceAll(":", ","); + + return this.sumNumbers(numbersPart.split(",")); + } + + const trimmed = str.trim(); + if (!/^[\d,:]+$/.test(trimmed)) { + throw new Error(App.ERROR_MESSAGES.INVALID_DELIMITER); + } + + // 기본 구문자 처리 + if (str.match(App.DEFAULT_DELIMITERS)) { + return this.sumNumbers(str.split(App.DEFAULT_DELIMITERS)); + } + if (!/^\d+$/.test(trimmed)) { + throw new Error(App.ERROR_MESSAGES.INVALID_DELIMITER); + } + + return this.sumNumbers([trimmed]); + } + + // 구분자로 분리된 문자열을 숫자로 변환하여 더하는 함수 + sumNumbers(stringNumbers) { + return stringNumbers.reduce((acc, num) => { + // 빈 문자열 처리 + if (num.trim() === "") return acc; + const parsed = parseInt(num.trim(), 10); + + // 음수 처리 + if (isNaN(parsed) || parsed < 0) { + throw new Error(App.ERROR_MESSAGES.INVALID_NUMBER); + } + return acc + parsed; + }, 0); + } + + printResult(result) { + MissionUtils.Console.print(`결과 : ${result}`); + } + + printError(error) { + MissionUtils.Console.print(error.message); + } } export default App;