From 0015006c5ff0b25a5c1d89d07162155b38c40a31 Mon Sep 17 00:00:00 2001 From: ghtjr410 Date: Mon, 29 Dec 2025 03:43:03 +0900 Subject: [PATCH 01/20] =?UTF-8?q?docs:=202=EB=8B=A8=EA=B3=84=20=EC=82=AC?= =?UTF-8?q?=EB=8B=A4=EB=A6=AC=20=EC=83=9D=EC=84=B1=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20README=20=EB=A7=81=ED=81=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit README.md - 2단계 문서 링크 누락된 부분을 `02-ladder-creation.md`로 정상 연결 docs/02-ladder-creation.md - 사다리 생성(2단계) 요구사항 문서 신규 추가 - 기능 요구사항, 프로그래밍 요구사항, 실행 예시, 힌트, PR 체크 항목 포함 docs/checklist.md - 미션 제출 전 체크리스트 문서 신규 추가 - OOP 원칙, 메서드 작성 원칙, TDD 및 테스트 품질 점검 항목 정리 --- README.md | 2 +- docs/02-ladder-creation.md | 56 ++++++++++++++++++++++++++++++++++++++ docs/checklist.md | 31 +++++++++++++++++++++ 3 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 docs/02-ladder-creation.md create mode 100644 docs/checklist.md diff --git a/README.md b/README.md index ff2aca185e..3029f06f78 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 본 저장소는 TDD 학습을 위한 단계별 과제를 포함합니다. ## 단계별 문서 - **[1단계 - 스트림, 람다, Optional](./docs/01-stream-lambda-optional.md)** -- **[2단계 - 사다리(생성)]()** +- **[2단계 - 사다리(생성)](./docs/02-ladder-creation.md)** - **[3단계 - 사다리(게임 실행)]()** - **[4단계 - 사다리(리팩터링)]()** diff --git a/docs/02-ladder-creation.md b/docs/02-ladder-creation.md new file mode 100644 index 0000000000..c2969954b4 --- /dev/null +++ b/docs/02-ladder-creation.md @@ -0,0 +1,56 @@ +# 2단계 - 사다리(생성) +*** +## 코드 리뷰 +> PR 링크: +> + +## 기능 요구사항 +- 사다리 게임에 참여하는 사람에 이름을 최대5글자까지 부여할 수 있다. 사다리를 출력할 때 사람 이름도 같이 출력한다. +- 사람 이름은 쉼표(,)를 기준으로 구분한다. +- 사람 이름을 5자 기준으로 출력하기 때문에 사다리 폭도 넓어져야 한다. +- 사다리 타기가 정상적으로 동작하려면 라인이 겹치지 않도록 해야 한다. +- |-----|-----| 모양과 같이 가로 라인이 겹치는 경우 어느 방향으로 이동할지 결정할 수 없다. + +## 프로그래밍 요구사항 +- **자바 8의 스트림과 람다를 적용해 프로그래밍한다.** +- **규칙 6: 모든 엔티티를 작게 유지한다.** + +### 실행 결과 +- 위 요구사항에 따라 4명의 사람을 위한 5개 높이 사다리를 만들 경우, 프로그램을 실행한 결과는 다음과 같다. +```text +참여할 사람 이름을 입력하세요. (이름은 쉼표(,)로 구분하세요) +pobi,honux,crong,jk + +최대 사다리 높이는 몇 개인가요? +5 + +실행결과 + +pobi honux crong jk + |-----| |-----| + | |-----| | + |-----| | | + | |-----| | + |-----| |-----| +``` + +### 힌트 +- 2차원 배열을 ArrayList, Generic을 적용해 구현하면 ArrayList>와 같이 이해하기 어려운 코드가 추가된다. +- 사다리 게임에서 한 라인의 좌표 값을 가지는 객체를 추가해 구현해 본다. +```java +public class Line { + private List points = new ArrayList<>(); + + public Line (int countOfPerson) { + // 라인의 좌표 값에 선이 있는지 유무를 판단하는 로직 추가 + } + + [...] +} +``` +- 위와 같이 Line 객체를 추가하면 ArrayList> 코드를 ArrayList과 같이 구현하는 것이 가능해 진다. + +## PR 전 점검 +**[체크리스트 확인하기](checklist.md)** + +## 구현 기능 목록 diff --git a/docs/checklist.md b/docs/checklist.md new file mode 100644 index 0000000000..745c30ea65 --- /dev/null +++ b/docs/checklist.md @@ -0,0 +1,31 @@ +# 미션 제출 전 체크리스트 + +### 1. 객체지향 생활체조 원칙 +- 규칙 1: 한 메서드에 오직 한 단계의 들여쓰기(indent)만 사용 +- 규칙 2: else 예약어 사용 금지 +- 규칙 3: 모든 원시값과 문자열을 포장 (Primitive Obsession 제거) +- 규칙 4: 한 줄에 점(.) 하나만 사용 (Law of Demeter) +- 규칙 5: 축약 금지 (명확한 네이밍) +- 규칙 6: 모든 엔티티 작게 유지 +- 규칙 7: 3개 이상의 인스턴스 변수를 가진 클래스 지양 +- 규칙 8: 일급 콜렉션 사용 +- 규칙 9: Getter/Setter 사용 지양 (Tell, Don't Ask) + +### 2. 메서드 작성 원칙 +- 한 가지 일만 수행하는 작은 메서드 (최대 10라인) + +### 2. 객체지향 설계 품질 +- 가장 작은 단위의 객체부터 시작 (상향식 개발) +- 객체에게 상태 노출 대신 책임 위임 +- 불변성을 최대한 유지 +- 도메인 용어로 네이밍 +- 협력 구조가 자연스럽게 읽힘 + +### 3. TDD +- TDD 사이클(Red-Green-Refactor)로 구현 +- TDD 사이클 단위 커밋 + +### 4. 테스트 품질 +- 객체의 책임과 결과만 검증 (구현 세부사항 테스트 지양) +- 단순 위임은 테스트 생략, 조건·분기·조합이 있다면 테스트 +- 테스트 객체 생성 헬퍼메서드가 반복되면 부생성자 도입 검토 From e71ce28e5aecc2b5a558c10ec937602cb9c9d7a2 Mon Sep 17 00:00:00 2001 From: ghtjr410 Date: Mon, 29 Dec 2025 03:53:51 +0900 Subject: [PATCH 02/20] =?UTF-8?q?feat:=20Name=20=EA=B0=92=20=EA=B0=9D?= =?UTF-8?q?=EC=B2=B4=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=9C=A0=ED=9A=A8?= =?UTF-8?q?=EC=84=B1=20=EA=B2=80=EC=A6=9D=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Name.java - 이름을 표현하는 `Name` 레코드 추가 - null, 빈 문자열, 공백 입력 시 예외 발생하도록 검증 로직 구현 NameTest.java - 빈 값(Null, Empty, Blank) 입력 시 예외 발생 테스트 추가 - 예외 메시지 검증 포함 --- .../java/nextstep/ladder/domain/Name.java | 10 +++++++++ .../java/nextstep/ladder/domain/NameTest.java | 22 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 src/main/java/nextstep/ladder/domain/Name.java create mode 100644 src/test/java/nextstep/ladder/domain/NameTest.java diff --git a/src/main/java/nextstep/ladder/domain/Name.java b/src/main/java/nextstep/ladder/domain/Name.java new file mode 100644 index 0000000000..b7919a81f4 --- /dev/null +++ b/src/main/java/nextstep/ladder/domain/Name.java @@ -0,0 +1,10 @@ +package nextstep.ladder.domain; + +public record Name(String value) { + + public Name { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException("이름은 필수입니다."); + } + } +} diff --git a/src/test/java/nextstep/ladder/domain/NameTest.java b/src/test/java/nextstep/ladder/domain/NameTest.java new file mode 100644 index 0000000000..b58c1c1449 --- /dev/null +++ b/src/test/java/nextstep/ladder/domain/NameTest.java @@ -0,0 +1,22 @@ +package nextstep.ladder.domain; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class NameTest { + + @ParameterizedTest(name = "빈값:{0}") + @NullAndEmptySource + @ValueSource(strings = " ") + void 생성자_빈값_예외발생(String input) { + assertThatThrownBy(() -> new Name(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("이름은 필수입니다."); + } +} From d57f6b73494c6531fa96676efaee660e2f0e275e Mon Sep 17 00:00:00 2001 From: ghtjr410 Date: Mon, 29 Dec 2025 03:58:53 +0900 Subject: [PATCH 03/20] =?UTF-8?q?feat:=20=EC=9D=B4=EB=A6=84=20=EA=B8=B8?= =?UTF-8?q?=EC=9D=B4=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=B3=B4?= =?UTF-8?q?=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Name.java - 최대 길이 상수 `MAX_LENGTH`(5) 추가 - 이름 공백·null 검증 시 입력값 포함하도록 예외 메시지 개선 - 이름이 5자를 초과할 경우 예외 발생 로직 추가 NameTest.java - 5자 초과 입력에 대한 예외 발생 테스트 케이스 추가 - 기존 필수값 검증 테스트 유지 --- src/main/java/nextstep/ladder/domain/Name.java | 7 ++++++- src/test/java/nextstep/ladder/domain/NameTest.java | 8 ++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/main/java/nextstep/ladder/domain/Name.java b/src/main/java/nextstep/ladder/domain/Name.java index b7919a81f4..e30e6374b0 100644 --- a/src/main/java/nextstep/ladder/domain/Name.java +++ b/src/main/java/nextstep/ladder/domain/Name.java @@ -1,10 +1,15 @@ package nextstep.ladder.domain; public record Name(String value) { + private static final int MAX_LENGTH = 5; public Name { if (value == null || value.isBlank()) { - throw new IllegalArgumentException("이름은 필수입니다."); + throw new IllegalArgumentException("이름은 필수입니다. 입력값: " + value); + } + + if (value.length() > 5) { + throw new IllegalArgumentException("이름은 %d자 이하여야 합니다. 입력값: %s".formatted(MAX_LENGTH, value)); } } } diff --git a/src/test/java/nextstep/ladder/domain/NameTest.java b/src/test/java/nextstep/ladder/domain/NameTest.java index b58c1c1449..266065a21e 100644 --- a/src/test/java/nextstep/ladder/domain/NameTest.java +++ b/src/test/java/nextstep/ladder/domain/NameTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.NullAndEmptySource; import org.junit.jupiter.params.provider.ValueSource; @@ -19,4 +20,11 @@ class NameTest { .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("이름은 필수입니다."); } + + @Test + void 생성자_5글자초과_예외발생() { + assertThatThrownBy(() -> new Name("123456")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("이름은 5자 이하여야 합니다."); + } } From 33a451dc19fd804203be2f03a921ae25c846cc00 Mon Sep 17 00:00:00 2001 From: ghtjr410 Date: Mon, 29 Dec 2025 08:28:42 +0900 Subject: [PATCH 04/20] =?UTF-8?q?feat:=20Names=20=EC=BB=AC=EB=A0=89?= =?UTF-8?q?=EC=85=98=20=EA=B0=92=20=EA=B0=9D=EC=B2=B4=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20=EB=B0=8F=20=ED=8C=8C=EC=8B=B1=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Names.java - 쉼표로 구분된 문자열을 `Name` 리스트로 변환하는 생성자 추가 - 불변 컬렉션 유지를 위해 `List.copyOf` 적용 - `Name` 리스트를 직접 받는 보조 생성자 구현 NamesTest.java - 문자열 입력을 쉼표 기준으로 파싱하여 `Names` 객체가 올바르게 생성되는지 검증 - `Name` 객체 리스트와의 동등성 비교 테스트 추가 --- src/main/java/nextstep/ladder/domain/Names.java | 15 +++++++++++++++ .../java/nextstep/ladder/domain/NamesTest.java | 17 +++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 src/main/java/nextstep/ladder/domain/Names.java create mode 100644 src/test/java/nextstep/ladder/domain/NamesTest.java diff --git a/src/main/java/nextstep/ladder/domain/Names.java b/src/main/java/nextstep/ladder/domain/Names.java new file mode 100644 index 0000000000..c15ccb4c47 --- /dev/null +++ b/src/main/java/nextstep/ladder/domain/Names.java @@ -0,0 +1,15 @@ +package nextstep.ladder.domain; + +import java.util.Arrays; +import java.util.List; + +public record Names(List values) { + + public Names(String input) { + this(Arrays.stream(input.split(",")).map(Name::new).toList()); + } + + public Names(List values) { + this.values = List.copyOf(values); + } +} diff --git a/src/test/java/nextstep/ladder/domain/NamesTest.java b/src/test/java/nextstep/ladder/domain/NamesTest.java new file mode 100644 index 0000000000..ba750f6440 --- /dev/null +++ b/src/test/java/nextstep/ladder/domain/NamesTest.java @@ -0,0 +1,17 @@ +package nextstep.ladder.domain; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class NamesTest { + + @Test + void 생성자_쉼표구분파싱() { + assertThat(new Names("사과,바나나")).isEqualTo(new Names(List.of(new Name("사과"), new Name("바나나")))); + } +} From 8e4bb2057689779d4bccbaaeb68fe90badd215c9 Mon Sep 17 00:00:00 2001 From: ghtjr410 Date: Mon, 29 Dec 2025 08:39:58 +0900 Subject: [PATCH 05/20] =?UTF-8?q?feat:=20Names=20=EC=B5=9C=EC=86=8C=20?= =?UTF-8?q?=EC=9D=B8=EC=9B=90=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=98=88=EC=99=B8=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Names.java - 최소 참가 인원 상수 `MIN_SIZE = 2` 추가 - `validate` 메서드로 null 입력 및 최소 인원(2명) 미만 입력 시 예외 처리 - 불변 리스트 유지 위해 `List.copyOf` 사용 NamesTest.java - null 리스트 전달 시 예외 발생 테스트 추가 - 참가자 1명 입력 시 예외 발생 테스트 추가 - 기존 쉼표 파싱 테스트 유지 --- src/main/java/nextstep/ladder/domain/Names.java | 13 +++++++++++++ .../java/nextstep/ladder/domain/NamesTest.java | 14 ++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/main/java/nextstep/ladder/domain/Names.java b/src/main/java/nextstep/ladder/domain/Names.java index c15ccb4c47..4ce7db4c42 100644 --- a/src/main/java/nextstep/ladder/domain/Names.java +++ b/src/main/java/nextstep/ladder/domain/Names.java @@ -4,12 +4,25 @@ import java.util.List; public record Names(List values) { + private static final int MIN_SIZE = 2; public Names(String input) { this(Arrays.stream(input.split(",")).map(Name::new).toList()); } public Names(List values) { + validate(values); + this.values = List.copyOf(values); } + + private void validate(List values) { + if (values == null) { + throw new IllegalArgumentException("참가자는 필수입니다."); + } + + if (values.size() < MIN_SIZE) { + throw new IllegalArgumentException("참가자는 %d명 이상이어야 합니다. 입력 인원: %d".formatted(MIN_SIZE, values.size())); + } + } } diff --git a/src/test/java/nextstep/ladder/domain/NamesTest.java b/src/test/java/nextstep/ladder/domain/NamesTest.java index ba750f6440..746e484266 100644 --- a/src/test/java/nextstep/ladder/domain/NamesTest.java +++ b/src/test/java/nextstep/ladder/domain/NamesTest.java @@ -14,4 +14,18 @@ class NamesTest { void 생성자_쉼표구분파싱() { assertThat(new Names("사과,바나나")).isEqualTo(new Names(List.of(new Name("사과"), new Name("바나나")))); } + + @Test + void 생성자_null_예외발생() { + assertThatThrownBy(() -> new Names((List) null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("참가자는 필수"); + } + + @Test + void 생성자_2명미만_예외발생() { + assertThatThrownBy(() -> new Names(List.of(new Name("사과")))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("참가자는 2명 이상"); + } } From 38033165049ce920256c481cdf6d5ae8eb916650 Mon Sep 17 00:00:00 2001 From: ghtjr410 Date: Tue, 30 Dec 2025 07:49:14 +0900 Subject: [PATCH 06/20] =?UTF-8?q?test:=20Name=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=EC=97=90=20=EC=A0=95=EC=83=81=20=EC=9E=85=EB=A0=A5=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BC=80=EC=9D=B4=EC=8A=A4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NameTest.java - 정상 입력("일", "일이삼사오")에 대해 생성자가 예외를 던지지 않음을 검증하는 파라미터 테스트 추가 --- src/test/java/nextstep/ladder/domain/NameTest.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/test/java/nextstep/ladder/domain/NameTest.java b/src/test/java/nextstep/ladder/domain/NameTest.java index 266065a21e..c8a51371d2 100644 --- a/src/test/java/nextstep/ladder/domain/NameTest.java +++ b/src/test/java/nextstep/ladder/domain/NameTest.java @@ -12,6 +12,12 @@ @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) class NameTest { + @ParameterizedTest(name = "올바른 값:{0}") + @ValueSource(strings = {"일", "일이삼사오"}) + void 생성자_정상입력_생성성공(String input) { + assertThatCode(() -> new Name(input)).doesNotThrowAnyException(); + } + @ParameterizedTest(name = "빈값:{0}") @NullAndEmptySource @ValueSource(strings = " ") From 377505e5923c4fc829e855381e5d36edc15d6ae3 Mon Sep 17 00:00:00 2001 From: ghtjr410 Date: Tue, 30 Dec 2025 07:49:44 +0900 Subject: [PATCH 07/20] =?UTF-8?q?feat:=20Line=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EA=B2=80=EC=A6=9D?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Line.java - 연속된 가로선(Boolean true)이 존재할 경우 예외 발생하도록 검증 로직 추가 - varargs 생성자 추가 및 내부 검증 실행 LineTest.java - 정상 입력 시 Line 생성 성공 여부 검증 - 연속된 가로선 입력 시 예외 발생 테스트 추가 --- .../java/nextstep/ladder/domain/Line.java | 24 +++++++++++++++++++ .../java/nextstep/ladder/domain/LineTest.java | 24 +++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 src/main/java/nextstep/ladder/domain/Line.java create mode 100644 src/test/java/nextstep/ladder/domain/LineTest.java diff --git a/src/main/java/nextstep/ladder/domain/Line.java b/src/main/java/nextstep/ladder/domain/Line.java new file mode 100644 index 0000000000..8d37b75785 --- /dev/null +++ b/src/main/java/nextstep/ladder/domain/Line.java @@ -0,0 +1,24 @@ +package nextstep.ladder.domain; + +import java.util.List; +import java.util.stream.IntStream; + +public record Line(List points) { + + public Line(Boolean... inputs) { + this(List.of(inputs)); + } + + public Line { + validate(points); + } + + private void validate(List points) { + boolean hasConsecutive = + IntStream.range(0, points.size() - 1).anyMatch(i -> points.get(i) && points.get(i + 1)); + + if (hasConsecutive) { + throw new IllegalArgumentException("가로선은 연속될 수 없습니다."); + } + } +} diff --git a/src/test/java/nextstep/ladder/domain/LineTest.java b/src/test/java/nextstep/ladder/domain/LineTest.java new file mode 100644 index 0000000000..32f190ca0a --- /dev/null +++ b/src/test/java/nextstep/ladder/domain/LineTest.java @@ -0,0 +1,24 @@ +package nextstep.ladder.domain; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class LineTest { + + @Test + void 생성자_정상입력_생성성공() { + assertThat(new Line(true, false, true).points()).containsExactly(true, false, true); + } + + @Test + void 생성자_연속된가로선_예외발생() { + assertThatThrownBy(() -> new Line(List.of(true, true))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("가로선은 연속될 수 없습니다."); + } +} From 5ec5b06a61366538f09703a2664eedf6a3abc7a2 Mon Sep 17 00:00:00 2001 From: ghtjr410 Date: Tue, 30 Dec 2025 07:54:37 +0900 Subject: [PATCH 08/20] =?UTF-8?q?feat:=20Line=20=EC=9E=85=EB=A0=A5=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EA=B0=95=ED=99=94=20=EB=B0=8F=20=EB=B9=88?= =?UTF-8?q?=EA=B0=92=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Line.java - 입력 리스트 null 또는 empty 여부 검증 추가 - 연속된 가로선 검증 로직에서 변수명 정리(points → inputs) LineTest.java - 빈값(null, empty) 입력 시 예외 발생 테스트 추가 - 검증 메시지 `"가로선 정보는 필수입니다."` 확인 로직 추가 --- src/main/java/nextstep/ladder/domain/Line.java | 8 ++++++-- src/test/java/nextstep/ladder/domain/LineTest.java | 10 ++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/main/java/nextstep/ladder/domain/Line.java b/src/main/java/nextstep/ladder/domain/Line.java index 8d37b75785..906488f956 100644 --- a/src/main/java/nextstep/ladder/domain/Line.java +++ b/src/main/java/nextstep/ladder/domain/Line.java @@ -13,9 +13,13 @@ public Line(Boolean... inputs) { validate(points); } - private void validate(List points) { + private void validate(List inputs) { + if (inputs == null || inputs.isEmpty()) { + throw new IllegalArgumentException("가로선 정보는 필수입니다."); + } + boolean hasConsecutive = - IntStream.range(0, points.size() - 1).anyMatch(i -> points.get(i) && points.get(i + 1)); + IntStream.range(0, inputs.size() - 1).anyMatch(i -> inputs.get(i) && inputs.get(i + 1)); if (hasConsecutive) { throw new IllegalArgumentException("가로선은 연속될 수 없습니다."); diff --git a/src/test/java/nextstep/ladder/domain/LineTest.java b/src/test/java/nextstep/ladder/domain/LineTest.java index 32f190ca0a..de260ef484 100644 --- a/src/test/java/nextstep/ladder/domain/LineTest.java +++ b/src/test/java/nextstep/ladder/domain/LineTest.java @@ -6,6 +6,8 @@ import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) class LineTest { @@ -15,6 +17,14 @@ class LineTest { assertThat(new Line(true, false, true).points()).containsExactly(true, false, true); } + @ParameterizedTest(name = "빈값:{0}") + @NullAndEmptySource + void 생성자_빈값_예외발생(List inputs) { + assertThatThrownBy(() -> new Line(inputs)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("가로선 정보는 필수입니다."); + } + @Test void 생성자_연속된가로선_예외발생() { assertThatThrownBy(() -> new Line(List.of(true, true))) From ffa6d71b38e414f5ffd7c098d03b1b80de8c6b4a Mon Sep 17 00:00:00 2001 From: ghtjr410 Date: Tue, 30 Dec 2025 08:00:38 +0900 Subject: [PATCH 09/20] =?UTF-8?q?feat:=20Height=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EC=B5=9C=EC=86=8C?= =?UTF-8?q?=20=EB=86=92=EC=9D=B4=20=EA=B2=80=EC=A6=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Height.java - 최소 높이(MIN_HEIGHT=1) 미만 입력 시 예외 발생하도록 검증 로직 구현 HeightTest.java - 정상 높이 입력 시 생성 성공 테스트 추가 - 1 미만 입력 시 예외 및 메시지 검증 테스트 추가 --- .../java/nextstep/ladder/domain/Height.java | 11 +++++++++ .../nextstep/ladder/domain/HeightTest.java | 23 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 src/main/java/nextstep/ladder/domain/Height.java create mode 100644 src/test/java/nextstep/ladder/domain/HeightTest.java diff --git a/src/main/java/nextstep/ladder/domain/Height.java b/src/main/java/nextstep/ladder/domain/Height.java new file mode 100644 index 0000000000..9bec07de16 --- /dev/null +++ b/src/main/java/nextstep/ladder/domain/Height.java @@ -0,0 +1,11 @@ +package nextstep.ladder.domain; + +public record Height(int value) { + private static final int MIN_HEIGHT = 1; + + public Height { + if (value < MIN_HEIGHT) { + throw new IllegalArgumentException("높이는 %d이상이어야 합니다. 입력값: %d".formatted(MIN_HEIGHT, value)); + } + } +} diff --git a/src/test/java/nextstep/ladder/domain/HeightTest.java b/src/test/java/nextstep/ladder/domain/HeightTest.java new file mode 100644 index 0000000000..b3bb18bc34 --- /dev/null +++ b/src/test/java/nextstep/ladder/domain/HeightTest.java @@ -0,0 +1,23 @@ +package nextstep.ladder.domain; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class HeightTest { + + @Test + void 생성자_정상입력_생성성공() { + assertThatCode(() -> new Height(1)).doesNotThrowAnyException(); + } + + @Test + void 생성자_1미만_예외발생() { + assertThatThrownBy(() -> new Height(0)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("높이는 1이상이어야 합니다."); + } +} From 5c0728a9bfbf577848642e2e67fa43ebff3d7dbc Mon Sep 17 00:00:00 2001 From: ghtjr410 Date: Tue, 30 Dec 2025 08:30:58 +0900 Subject: [PATCH 10/20] =?UTF-8?q?feat:=20Lines=20=EB=B0=8F=20LineGenerator?= =?UTF-8?q?=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LineGenerator.java - Line 생성을 위한 함수형 인터페이스 정의 Lines.java - Height와 personCount 기반으로 Line 리스트 자동 생성 로직 구현 - 생성된 Line 리스트 불변화하여 보관 LinesTest.java - Height 값만큼 Line이 생성되는지 검증 테스트 추가 --- .../nextstep/ladder/domain/LineGenerator.java | 6 +++++ .../java/nextstep/ladder/domain/Lines.java | 25 +++++++++++++++++++ .../nextstep/ladder/domain/LinesTest.java | 16 ++++++++++++ 3 files changed, 47 insertions(+) create mode 100644 src/main/java/nextstep/ladder/domain/LineGenerator.java create mode 100644 src/main/java/nextstep/ladder/domain/Lines.java create mode 100644 src/test/java/nextstep/ladder/domain/LinesTest.java diff --git a/src/main/java/nextstep/ladder/domain/LineGenerator.java b/src/main/java/nextstep/ladder/domain/LineGenerator.java new file mode 100644 index 0000000000..a27f350010 --- /dev/null +++ b/src/main/java/nextstep/ladder/domain/LineGenerator.java @@ -0,0 +1,6 @@ +package nextstep.ladder.domain; + +@FunctionalInterface +public interface LineGenerator { + Line generate(int count); +} diff --git a/src/main/java/nextstep/ladder/domain/Lines.java b/src/main/java/nextstep/ladder/domain/Lines.java new file mode 100644 index 0000000000..233308dcf0 --- /dev/null +++ b/src/main/java/nextstep/ladder/domain/Lines.java @@ -0,0 +1,25 @@ +package nextstep.ladder.domain; + +import java.util.List; +import java.util.stream.IntStream; + +public record Lines(List values) { + + public Lines(int height, int personCount, LineGenerator generator) { + this(new Height(height), personCount, generator); + } + + public Lines(Height height, int personCount, LineGenerator generator) { + this(generate(height, personCount, generator)); + } + + private static List generate(Height height, int personCount, LineGenerator generator) { + return IntStream.range(0, height.value()) + .mapToObj(i -> generator.generate(personCount - 1)) + .toList(); + } + + public Lines(List values) { + this.values = List.copyOf(values); + } +} diff --git a/src/test/java/nextstep/ladder/domain/LinesTest.java b/src/test/java/nextstep/ladder/domain/LinesTest.java new file mode 100644 index 0000000000..f2a191e969 --- /dev/null +++ b/src/test/java/nextstep/ladder/domain/LinesTest.java @@ -0,0 +1,16 @@ +package nextstep.ladder.domain; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class LinesTest { + + @Test + void 생성자_Height만큼_Line생성() { + assertThat(new Lines(3, 3, count -> new Line(true, false)).values()).hasSize(3); + } +} From d3f921738642737f26c600cc618add8b08c4af0b Mon Sep 17 00:00:00 2001 From: ghtjr410 Date: Tue, 30 Dec 2025 08:49:13 +0900 Subject: [PATCH 11/20] =?UTF-8?q?feat:=20Line=20=EC=B6=9C=EB=A0=A5=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Line.java - points 리스트 불변화 처리 - CONNECTED/EMPTY 문자열 상수 추가 - toDisplay() 메서드로 가로선 출력 문자열 생성 기능 구현 - Boolean 값에 따른 segment 변환 로직 추가 LineTest.java - toDisplay() 결과가 기대 문자열과 일치하는지 테스트 추가 --- src/main/java/nextstep/ladder/domain/Line.java | 13 +++++++++++++ src/test/java/nextstep/ladder/domain/LineTest.java | 5 +++++ 2 files changed, 18 insertions(+) diff --git a/src/main/java/nextstep/ladder/domain/Line.java b/src/main/java/nextstep/ladder/domain/Line.java index 906488f956..f7ee1d12af 100644 --- a/src/main/java/nextstep/ladder/domain/Line.java +++ b/src/main/java/nextstep/ladder/domain/Line.java @@ -1,9 +1,12 @@ package nextstep.ladder.domain; import java.util.List; +import java.util.stream.Collectors; import java.util.stream.IntStream; public record Line(List points) { + private static final String CONNECTED = "-----"; + private static final String EMPTY = " "; public Line(Boolean... inputs) { this(List.of(inputs)); @@ -11,6 +14,7 @@ public Line(Boolean... inputs) { public Line { validate(points); + points = List.copyOf(points); } private void validate(List inputs) { @@ -25,4 +29,13 @@ private void validate(List inputs) { throw new IllegalArgumentException("가로선은 연속될 수 없습니다."); } } + + public String toDisplay() { + String body = points.stream().map(this::toSegment).collect(Collectors.joining("|")); + return "|" + body + "|"; + } + + private String toSegment(boolean connected) { + return connected ? CONNECTED : EMPTY; + } } diff --git a/src/test/java/nextstep/ladder/domain/LineTest.java b/src/test/java/nextstep/ladder/domain/LineTest.java index de260ef484..1c69f206a6 100644 --- a/src/test/java/nextstep/ladder/domain/LineTest.java +++ b/src/test/java/nextstep/ladder/domain/LineTest.java @@ -31,4 +31,9 @@ class LineTest { .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("가로선은 연속될 수 없습니다."); } + + @Test + void toDisplay() { + assertThat(new Line(true, false, true).toDisplay()).isEqualTo("|-----| |-----|"); + } } From 4ad601b0d2ca11e12700524123f29128188f9eec Mon Sep 17 00:00:00 2001 From: ghtjr410 Date: Tue, 30 Dec 2025 08:53:36 +0900 Subject: [PATCH 12/20] =?UTF-8?q?feat:=20Lines=20=EC=B6=9C=EB=A0=A5=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lines.java - Lines 전체를 문자열로 변환하는 toDisplay() 메서드 추가 - 각 Line의 toDisplay() 결과를 개행 문자로 이어 붙여 출력 LinesTest.java - toDisplay()가 기대한 멀티라인 문자열을 생성하는지 검증 테스트 추가 --- src/main/java/nextstep/ladder/domain/Lines.java | 5 +++++ src/test/java/nextstep/ladder/domain/LinesTest.java | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/src/main/java/nextstep/ladder/domain/Lines.java b/src/main/java/nextstep/ladder/domain/Lines.java index 233308dcf0..8f713bada6 100644 --- a/src/main/java/nextstep/ladder/domain/Lines.java +++ b/src/main/java/nextstep/ladder/domain/Lines.java @@ -1,6 +1,7 @@ package nextstep.ladder.domain; import java.util.List; +import java.util.stream.Collectors; import java.util.stream.IntStream; public record Lines(List values) { @@ -22,4 +23,8 @@ private static List generate(Height height, int personCount, LineGenerator public Lines(List values) { this.values = List.copyOf(values); } + + public String toDisplay() { + return values.stream().map(Line::toDisplay).collect(Collectors.joining("\n")); + } } diff --git a/src/test/java/nextstep/ladder/domain/LinesTest.java b/src/test/java/nextstep/ladder/domain/LinesTest.java index f2a191e969..b550182711 100644 --- a/src/test/java/nextstep/ladder/domain/LinesTest.java +++ b/src/test/java/nextstep/ladder/domain/LinesTest.java @@ -13,4 +13,10 @@ class LinesTest { void 생성자_Height만큼_Line생성() { assertThat(new Lines(3, 3, count -> new Line(true, false)).values()).hasSize(3); } + + @Test + void toDisplay() { + assertThat(new Lines(2, 3, count -> new Line(true, false)).toDisplay()) + .isEqualTo("|-----| |\n|-----| |"); + } } From 84d6dedc0e37761fc573645557358d0880968969 Mon Sep 17 00:00:00 2001 From: ghtjr410 Date: Tue, 30 Dec 2025 19:56:57 +0900 Subject: [PATCH 13/20] =?UTF-8?q?feat:=20=EC=9D=B4=EB=A6=84/=EC=B0=B8?= =?UTF-8?q?=EA=B0=80=EC=9E=90=20=EB=AA=A9=EB=A1=9D=20=EC=B6=9C=EB=A0=A5?= =?UTF-8?q?=EC=9A=A9=20toDisplay=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Name.java - 이름을 6자 고정 길이로 오른쪽 공백을 채워 반환하는 toDisplay() 추가 Names.java - 각 Name의 toDisplay()를 이어붙여 출력 문자열을 생성하는 toDisplay() 추가 NameTest.java - toDisplay()가 6자 고정 길이로 공백을 채우는지 검증 테스트 추가 NamesTest.java - Names의 toDisplay() 결과가 참가자 이름들을 공백 포함 정렬하여 반환하는지 검증 테스트 추가 --- src/main/java/nextstep/ladder/domain/Name.java | 4 ++++ src/main/java/nextstep/ladder/domain/Names.java | 5 +++++ src/test/java/nextstep/ladder/domain/NameTest.java | 5 +++++ src/test/java/nextstep/ladder/domain/NamesTest.java | 5 +++++ 4 files changed, 19 insertions(+) diff --git a/src/main/java/nextstep/ladder/domain/Name.java b/src/main/java/nextstep/ladder/domain/Name.java index e30e6374b0..ab432e80c3 100644 --- a/src/main/java/nextstep/ladder/domain/Name.java +++ b/src/main/java/nextstep/ladder/domain/Name.java @@ -12,4 +12,8 @@ public record Name(String value) { throw new IllegalArgumentException("이름은 %d자 이하여야 합니다. 입력값: %s".formatted(MAX_LENGTH, value)); } } + + public String toDisplay() { + return String.format("%-6s", value); + } } diff --git a/src/main/java/nextstep/ladder/domain/Names.java b/src/main/java/nextstep/ladder/domain/Names.java index 4ce7db4c42..81ff3cc75e 100644 --- a/src/main/java/nextstep/ladder/domain/Names.java +++ b/src/main/java/nextstep/ladder/domain/Names.java @@ -2,6 +2,7 @@ import java.util.Arrays; import java.util.List; +import java.util.stream.Collectors; public record Names(List values) { private static final int MIN_SIZE = 2; @@ -25,4 +26,8 @@ private void validate(List values) { throw new IllegalArgumentException("참가자는 %d명 이상이어야 합니다. 입력 인원: %d".formatted(MIN_SIZE, values.size())); } } + + public String toDisplay() { + return values.stream().map(Name::toDisplay).collect(Collectors.joining()); + } } diff --git a/src/test/java/nextstep/ladder/domain/NameTest.java b/src/test/java/nextstep/ladder/domain/NameTest.java index c8a51371d2..297c8eed07 100644 --- a/src/test/java/nextstep/ladder/domain/NameTest.java +++ b/src/test/java/nextstep/ladder/domain/NameTest.java @@ -33,4 +33,9 @@ class NameTest { .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("이름은 5자 이하여야 합니다."); } + + @Test + void toDisplay_6자고정_오른쪽공백채움() { + assertThat(new Name("사과").toDisplay()).isEqualTo("사과 "); + } } diff --git a/src/test/java/nextstep/ladder/domain/NamesTest.java b/src/test/java/nextstep/ladder/domain/NamesTest.java index 746e484266..2f0310a1c4 100644 --- a/src/test/java/nextstep/ladder/domain/NamesTest.java +++ b/src/test/java/nextstep/ladder/domain/NamesTest.java @@ -28,4 +28,9 @@ class NamesTest { .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("참가자는 2명 이상"); } + + @Test + void toDisplay() { + assertThat(new Names("맛있는사과,바나나,키위").toDisplay()).isEqualTo("맛있는사과 바나나 키위 "); + } } From bb95a11eb3c2728435a3c5f2fab546b843ffe4e4 Mon Sep 17 00:00:00 2001 From: ghtjr410 Date: Tue, 30 Dec 2025 19:59:12 +0900 Subject: [PATCH 14/20] =?UTF-8?q?refactor:=20=EC=82=AC=EB=8B=A4=EB=A6=AC?= =?UTF-8?q?=20=EC=A4=84(Line)=20=EC=B6=9C=EB=A0=A5=20=ED=8F=AC=EB=A7=B7=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Line.java - 세그먼트 문자열(CONNECTED/EMPTY)에 `|` 포함하도록 변경 - toDisplay()에서 구분자 제거 후 연속 문자열로 생성 - 출력 시작 부분을 `" |"`로 통일하여 정렬 형식 변경 LineTest.java - 변경된 출력 포맷에 맞춰 toDisplay() 기대값 수정 LinesTest.java - 여러 줄 출력 시 앞쪽 여백 추가된 새로운 포맷에 맞게 기대값 수정 --- src/main/java/nextstep/ladder/domain/Line.java | 8 ++++---- src/test/java/nextstep/ladder/domain/LineTest.java | 2 +- src/test/java/nextstep/ladder/domain/LinesTest.java | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/nextstep/ladder/domain/Line.java b/src/main/java/nextstep/ladder/domain/Line.java index f7ee1d12af..22486906e8 100644 --- a/src/main/java/nextstep/ladder/domain/Line.java +++ b/src/main/java/nextstep/ladder/domain/Line.java @@ -5,8 +5,8 @@ import java.util.stream.IntStream; public record Line(List points) { - private static final String CONNECTED = "-----"; - private static final String EMPTY = " "; + private static final String CONNECTED = "-----|"; + private static final String EMPTY = " |"; public Line(Boolean... inputs) { this(List.of(inputs)); @@ -31,8 +31,8 @@ private void validate(List inputs) { } public String toDisplay() { - String body = points.stream().map(this::toSegment).collect(Collectors.joining("|")); - return "|" + body + "|"; + String body = points.stream().map(this::toSegment).collect(Collectors.joining()); + return " |" + body; } private String toSegment(boolean connected) { diff --git a/src/test/java/nextstep/ladder/domain/LineTest.java b/src/test/java/nextstep/ladder/domain/LineTest.java index 1c69f206a6..27934e9463 100644 --- a/src/test/java/nextstep/ladder/domain/LineTest.java +++ b/src/test/java/nextstep/ladder/domain/LineTest.java @@ -34,6 +34,6 @@ class LineTest { @Test void toDisplay() { - assertThat(new Line(true, false, true).toDisplay()).isEqualTo("|-----| |-----|"); + assertThat(new Line(true, false, true).toDisplay()).isEqualTo(" |-----| |-----|"); } } diff --git a/src/test/java/nextstep/ladder/domain/LinesTest.java b/src/test/java/nextstep/ladder/domain/LinesTest.java index b550182711..189d8c3380 100644 --- a/src/test/java/nextstep/ladder/domain/LinesTest.java +++ b/src/test/java/nextstep/ladder/domain/LinesTest.java @@ -17,6 +17,6 @@ class LinesTest { @Test void toDisplay() { assertThat(new Lines(2, 3, count -> new Line(true, false)).toDisplay()) - .isEqualTo("|-----| |\n|-----| |"); + .isEqualTo(" |-----| |\n |-----| |"); } } From 44dc2c7a045f2b4429f118243473e606f45c6423 Mon Sep 17 00:00:00 2001 From: ghtjr410 Date: Tue, 30 Dec 2025 20:03:16 +0900 Subject: [PATCH 15/20] =?UTF-8?q?feat:=20Ladder=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EC=A0=84=EC=B2=B4?= =?UTF-8?q?=20=EC=B6=9C=EB=A0=A5(toDisplay)=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ladder.java - Names와 Lines를 결합한 Ladder 레코드 추가 - names.toDisplay()와 lines.toDisplay()를 조합하여 전체 사다리 출력 문자열 생성 LadderTest.java - Ladder.toDisplay()가 이름 영역 + 사다리 영역을 올바르게 연결하는지 검증 테스트 추가 --- .../java/nextstep/ladder/domain/Ladder.java | 8 ++++++++ .../nextstep/ladder/domain/LadderTest.java | 20 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 src/main/java/nextstep/ladder/domain/Ladder.java create mode 100644 src/test/java/nextstep/ladder/domain/LadderTest.java diff --git a/src/main/java/nextstep/ladder/domain/Ladder.java b/src/main/java/nextstep/ladder/domain/Ladder.java new file mode 100644 index 0000000000..0c6f1515bc --- /dev/null +++ b/src/main/java/nextstep/ladder/domain/Ladder.java @@ -0,0 +1,8 @@ +package nextstep.ladder.domain; + +public record Ladder(Names names, Lines lines) { + + public String toDisplay() { + return names.toDisplay() + "\n" + lines.toDisplay(); + } +} diff --git a/src/test/java/nextstep/ladder/domain/LadderTest.java b/src/test/java/nextstep/ladder/domain/LadderTest.java new file mode 100644 index 0000000000..12838b38ef --- /dev/null +++ b/src/test/java/nextstep/ladder/domain/LadderTest.java @@ -0,0 +1,20 @@ +package nextstep.ladder.domain; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class LadderTest { + @Test + void toDisplay() { + Names names = new Names("pobi,honux,jk"); + Lines lines = new Lines(2, 3, count -> new Line(true, false)); + Ladder ladder = new Ladder(names, lines); + + assertThat(ladder.toDisplay()) + .isEqualTo("pobi honux jk \n" + " |-----| |\n" + " |-----| |"); + } +} From 1c43edcb735cb9bed812db38166f32c95c3db8e3 Mon Sep 17 00:00:00 2001 From: ghtjr410 Date: Tue, 30 Dec 2025 20:33:29 +0900 Subject: [PATCH 16/20] =?UTF-8?q?feat:=20=EB=9E=9C=EB=8D=A4=20=EC=82=AC?= =?UTF-8?q?=EB=8B=A4=EB=A6=AC=20=EB=9D=BC=EC=9D=B8=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BooleanGenerator.java - 불리언 값을 공급하는 FunctionalInterface 추가 RandomLineGenerator.java - 연속된 true 방지를 포함한 랜덤 라인 생성 로직 구현 - BooleanGenerator 주입 가능하도록 생성자 오버로드 추가 RandomLineGeneratorTest.java - 주입된 BooleanGenerator 시퀀스를 사용해 연속 true 방지 동작 검증 --- .../ladder/domain/BooleanGenerator.java | 6 ++++ .../ladder/domain/RandomLineGenerator.java | 30 +++++++++++++++++++ .../domain/RandomLineGeneratorTest.java | 23 ++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 src/main/java/nextstep/ladder/domain/BooleanGenerator.java create mode 100644 src/main/java/nextstep/ladder/domain/RandomLineGenerator.java create mode 100644 src/test/java/nextstep/ladder/domain/RandomLineGeneratorTest.java diff --git a/src/main/java/nextstep/ladder/domain/BooleanGenerator.java b/src/main/java/nextstep/ladder/domain/BooleanGenerator.java new file mode 100644 index 0000000000..5ee9898072 --- /dev/null +++ b/src/main/java/nextstep/ladder/domain/BooleanGenerator.java @@ -0,0 +1,6 @@ +package nextstep.ladder.domain; + +@FunctionalInterface +public interface BooleanGenerator { + boolean generate(); +} diff --git a/src/main/java/nextstep/ladder/domain/RandomLineGenerator.java b/src/main/java/nextstep/ladder/domain/RandomLineGenerator.java new file mode 100644 index 0000000000..9ccbe1ab9e --- /dev/null +++ b/src/main/java/nextstep/ladder/domain/RandomLineGenerator.java @@ -0,0 +1,30 @@ +package nextstep.ladder.domain; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +public class RandomLineGenerator implements LineGenerator { + private final BooleanGenerator booleanGenerator; + + public RandomLineGenerator() { + this(new Random()::nextBoolean); + } + + public RandomLineGenerator(BooleanGenerator booleanGenerator) { + this.booleanGenerator = booleanGenerator; + } + + @Override + public Line generate(int count) { + List points = new ArrayList<>(); + + for (int i = 0; i < count; i++) { + boolean prev = i > 0 && points.get(i - 1); + boolean current = !prev && booleanGenerator.generate(); + points.add(current); + } + + return new Line(points); + } +} diff --git a/src/test/java/nextstep/ladder/domain/RandomLineGeneratorTest.java b/src/test/java/nextstep/ladder/domain/RandomLineGeneratorTest.java new file mode 100644 index 0000000000..81f9e4f132 --- /dev/null +++ b/src/test/java/nextstep/ladder/domain/RandomLineGeneratorTest.java @@ -0,0 +1,23 @@ +package nextstep.ladder.domain; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Iterator; +import java.util.List; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class RandomLineGeneratorTest { + + @Test + void generate_연속true_방지() { + Iterator values = List.of(true, true, true).iterator(); + BooleanGenerator generator = values::next; + + Line line = new RandomLineGenerator(generator).generate(3); + + assertThat(line.points()).containsExactly(true, false, true); + } +} From 768993e74dd878e43ec1ae422fff6cafca748118 Mon Sep 17 00:00:00 2001 From: ghtjr410 Date: Tue, 30 Dec 2025 20:44:38 +0900 Subject: [PATCH 17/20] =?UTF-8?q?feat:=20=EC=82=AC=EB=8B=A4=EB=A6=AC=20?= =?UTF-8?q?=EA=B2=8C=EC=9E=84=20=EC=8B=A4=ED=96=89=EC=9A=A9=20Application?= =?UTF-8?q?=20=EB=B0=8F=20=EC=9E=85=EC=B6=9C=EB=A0=A5=20=EB=B7=B0=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Application.java - 사용자 입력을 받아 Names, Height 생성 - Lines 생성 후 Ladder 구성 - 결과 출력 처리 Names.java - 참가자 수 반환용 size() 메서드 추가 InputView.java - 이름 목록 입력 기능 추가 - 사다리 높이 입력 기능 추가 OutputView.java - 사다리 출력 결과 표시 기능 추가 --- src/main/java/nextstep/ladder/Application.java | 17 +++++++++++++++++ src/main/java/nextstep/ladder/domain/Names.java | 4 ++++ .../java/nextstep/ladder/view/InputView.java | 17 +++++++++++++++++ .../java/nextstep/ladder/view/OutputView.java | 11 +++++++++++ 4 files changed, 49 insertions(+) create mode 100644 src/main/java/nextstep/ladder/Application.java create mode 100644 src/main/java/nextstep/ladder/view/InputView.java create mode 100644 src/main/java/nextstep/ladder/view/OutputView.java diff --git a/src/main/java/nextstep/ladder/Application.java b/src/main/java/nextstep/ladder/Application.java new file mode 100644 index 0000000000..ee038ebf49 --- /dev/null +++ b/src/main/java/nextstep/ladder/Application.java @@ -0,0 +1,17 @@ +package nextstep.ladder; + +import nextstep.ladder.domain.*; +import nextstep.ladder.view.InputView; +import nextstep.ladder.view.OutputView; + +public class Application { + public static void main(String[] args) { + Names names = new Names(InputView.readNames()); + Height height = new Height(InputView.readHeight()); + + Lines lines = new Lines(height, names.size(), new RandomLineGenerator()); + Ladder ladder = new Ladder(names, lines); + + OutputView.printResult(ladder); + } +} diff --git a/src/main/java/nextstep/ladder/domain/Names.java b/src/main/java/nextstep/ladder/domain/Names.java index 81ff3cc75e..9293621ddf 100644 --- a/src/main/java/nextstep/ladder/domain/Names.java +++ b/src/main/java/nextstep/ladder/domain/Names.java @@ -27,6 +27,10 @@ private void validate(List values) { } } + public int size() { + return values.size(); + } + public String toDisplay() { return values.stream().map(Name::toDisplay).collect(Collectors.joining()); } diff --git a/src/main/java/nextstep/ladder/view/InputView.java b/src/main/java/nextstep/ladder/view/InputView.java new file mode 100644 index 0000000000..f540beeabe --- /dev/null +++ b/src/main/java/nextstep/ladder/view/InputView.java @@ -0,0 +1,17 @@ +package nextstep.ladder.view; + +import java.util.Scanner; + +public class InputView { + private static final Scanner SCANNER = new Scanner(System.in); + + public static String readNames() { + System.out.println("참여할 사람 이름을 입력하세요. (이름은 쉼표(,)로 구분하세요)"); + return SCANNER.nextLine(); + } + + public static int readHeight() { + System.out.println("최대 사다리 높이는 몇 개인가요?"); + return Integer.parseInt(SCANNER.nextLine()); + } +} diff --git a/src/main/java/nextstep/ladder/view/OutputView.java b/src/main/java/nextstep/ladder/view/OutputView.java new file mode 100644 index 0000000000..3062629f26 --- /dev/null +++ b/src/main/java/nextstep/ladder/view/OutputView.java @@ -0,0 +1,11 @@ +package nextstep.ladder.view; + +import nextstep.ladder.domain.Ladder; + +public class OutputView { + + public static void printResult(Ladder ladder) { + System.out.println("\n실행결과\n"); + System.out.println(ladder.toDisplay()); + } +} From ebdfe802fc2f6e4fa2a9526e83d431f476689da8 Mon Sep 17 00:00:00 2001 From: ghtjr410 Date: Tue, 30 Dec 2025 20:59:09 +0900 Subject: [PATCH 18/20] =?UTF-8?q?docs:=20=EC=82=AC=EB=8B=A4=EB=A6=AC=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EA=B8=B0=EB=8A=A5=20=EC=A0=84=EC=B2=B4=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EB=AA=A9=EB=A1=9D=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 02-ladder-creation.md - 참가자 이름/이름 목록 요구사항 정리 추가 - 높이(Height) 검증 및 기능 명세 추가 - Line·Lines 생성 및 출력 규칙 문서화 - LineGenerator·BooleanGenerator·RandomLineGenerator 기능 설명 추가 - Ladder 구성 및 출력 기능 목록 정리 --- docs/02-ladder-creation.md | 51 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/docs/02-ladder-creation.md b/docs/02-ladder-creation.md index c2969954b4..302adeb22c 100644 --- a/docs/02-ladder-creation.md +++ b/docs/02-ladder-creation.md @@ -54,3 +54,54 @@ public class Line { **[체크리스트 확인하기](checklist.md)** ## 구현 기능 목록 + +### 참가자 이름 +- [x] 이름 (Name) + - [x] 문자열로 생성 + - [x] null 또는 빈 값일 시 예외 발생 + - [x] 5자 초과 시 예외 발생 + - [x] 6자 고정 출력용 문자열 반환 + +- [x] 이름 목록 (Names) + - [x] 쉼표 구분 문자열로 생성 + - [x] 이름 목록으로 생성 + - [x] null일 시 예외 발생 + - [x] 2명 미만일 시 예외 발생 + - [x] 참가자 수 반환 + - [x] 출력용 문자열 반환 + +### 사다리 높이 +- [x] 높이 (Height) + - [x] 정수로 생성 + - [x] 1 미만일 시 예외 발생 + +### 사다리 라인 +- [x] 라인 (Line) + - [x] Boolean 목록으로 생성 + - [x] 가변인자로 생성 + - [x] null 또는 빈 값일 시 예외 발생 + - [x] 연속된 true 존재 시 예외 발생 + - [x] 출력용 문자열 반환 + +- [x] 라인 목록 (Lines) + - [x] Line 목록으로 생성 + - [x] Height, 참가자 수, LineGenerator로 생성 + - [x] 출력용 문자열 반환 + +### 사다리 생성 +- [x] 라인 생성기 인터페이스 (LineGenerator) + - [x] Line 생성 메서드 정의 + +- [x] Boolean 생성기 인터페이스 (BooleanGenerator) + - [x] boolean 생성 메서드 정의 + +- [x] 랜덤 라인 생성기 (RandomLineGenerator) + - [x] LineGenerator 구현 + - [x] BooleanGenerator 주입 가능 + - [x] 기본 생성 시 Random 사용 + - [x] 연속 true 방지 로직 적용 + +### 사다리 +- [x] 사다리 (Ladder) + - [x] Names, Lines로 생성 + - [x] 출력용 문자열 반환 From 0b4fd6ab97906b15ee381ea75cf05dfb49a9db7f Mon Sep 17 00:00:00 2001 From: ghtjr410 Date: Tue, 30 Dec 2025 21:05:33 +0900 Subject: [PATCH 19/20] =?UTF-8?q?fix:=20=EC=9D=B4=EB=A6=84=20=EA=B8=B8?= =?UTF-8?q?=EC=9D=B4=20=EA=B2=80=EC=A6=9D=20=EC=8B=9C=20=ED=95=98=EB=93=9C?= =?UTF-8?q?=EC=BD=94=EB=94=A9=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Name.java - 최대 길이 검증 로직에서 하드코딩된 `5` 제거 - `MAX_LENGTH` 상수를 사용하도록 수정 --- src/main/java/nextstep/ladder/domain/Name.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/nextstep/ladder/domain/Name.java b/src/main/java/nextstep/ladder/domain/Name.java index ab432e80c3..e823628572 100644 --- a/src/main/java/nextstep/ladder/domain/Name.java +++ b/src/main/java/nextstep/ladder/domain/Name.java @@ -8,7 +8,7 @@ public record Name(String value) { throw new IllegalArgumentException("이름은 필수입니다. 입력값: " + value); } - if (value.length() > 5) { + if (value.length() > MAX_LENGTH) { throw new IllegalArgumentException("이름은 %d자 이하여야 합니다. 입력값: %s".formatted(MAX_LENGTH, value)); } } From a548930555025d8dbaa9a778a143b774b836fa6d Mon Sep 17 00:00:00 2001 From: ghtjr410 Date: Tue, 30 Dec 2025 21:19:01 +0900 Subject: [PATCH 20/20] =?UTF-8?q?docs:=20PR=20=EB=A7=81=ED=81=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/02-ladder-creation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/02-ladder-creation.md b/docs/02-ladder-creation.md index 302adeb22c..c8dd41c03f 100644 --- a/docs/02-ladder-creation.md +++ b/docs/02-ladder-creation.md @@ -2,7 +2,7 @@ *** ## 코드 리뷰 > PR 링크: -> +> **[https://github.com/next-step/java-ladder/pull/2431](https://github.com/next-step/java-ladder/pull/2431)** ## 기능 요구사항 - 사다리 게임에 참여하는 사람에 이름을 최대5글자까지 부여할 수 있다. 사다리를 출력할 때 사람 이름도 같이 출력한다.