Skip to content

Commit 654d675

Browse files
helenasontakoyakimchitakoyakimchi
authored
[1단계 - 사다리 생성] 에버(손채영) 미션 제출합니다. (#316)
* docs: 구현할 기능 요구사항 작성 Co-authored-by: takoyakimchi <[email protected]> * docs: 참여자 이름 관련 요구사항 수정 Co-authored-by: takoyakimchi <[email protected]> * test: 참여자 이름 테스트 코드 작성 Co-authored-by: takoyakimchi <[email protected]> * test: 비어있는 이름에 대한 에러메시지 수정 및 문법 관련 에러 해결 Co-authored-by: takoyakimchi <[email protected]> * feat: 참여자 이름 도메인 생성 Co-authored-by: takoyakimchi <c4nyou결@gmail.com> * test: 참여자들 테스트 코드 작성 Co-authored-by: takoyakimchi <[email protected]> * test: 참여자 테스트 코드 작성 Co-authored-by: takoyakimchi <[email protected]> * feat: 멤버 도메인 생성 Co-authored-by: takoyakimchi <[email protected]> * feat: 참여자 이름 가져오는 메서드 추가 Co-authored-by: takoyakimchi <[email protected]> * test: 참여자들 테스트 null 입력하는 경우 추가 및 에러 메시지 검증 제외 Co-authored-by: takoyakimchi <[email protected]> * feat: 참여자들 도메인 생성 Co-authored-by: takoyakimchi <[email protected]> * test: 가로줄 테스트 코드 작성 Co-authored-by: takoyakimchi <[email protected]> * feat: 가로줄 도메인 및 가로줄 생성을 위한 전략 인터페이스 생성 Co-authored-by: takoyakimchi <[email protected]> * test: 가로줄 테스트 도메인에 맞게 수정 Co-authored-by: takoyakimchi <[email protected]> * test: 사다리 높이 도메인 테스트 코드 작성 Co-authored-by: takoyakimchi <[email protected]> * feat: 사다리 높이 도메인 생성 Co-authored-by: takoyakimchi <[email protected]> * test: 사다리 전체 도메인 테스트 코드 작성 Co-authored-by: takoyakimchi <[email protected]> * feat: 사다리 전체 도메인 생성 Co-authored-by: takoyakimchi <[email protected]> * feat: 사다리 높이 도메인 getter 추가 Co-authored-by: takoyakimchi <[email protected]> * test: 사다리 전체 도메인에 맞게 수정 Co-authored-by: takoyakimchi <[email protected]> * feat: view 클래스 생성 Co-authored-by: takoyakimchi <[email protected]> * feat: 출력 로직 구현 Co-authored-by: takoyakimchi <[email protected]> * feat: 참여자들 도메인에 데이터 반환 메서드 추가 Co-authored-by: takoyakimchi <[email protected]> * feat: 게임 실행 도메인 생성 Co-authored-by: takoyakimchi <[email protected]> * feat: 컨트롤러 생성 Co-authored-by: takoyakimchi <[email protected]> * feat: main 메서드 생성 Co-authored-by: takoyakimchi <[email protected]> * style: 공백 정리 Co-authored-by: takoyakimchi <[email protected]> * feat: 에러 발생 시 재입력받는 로직 추가 Co-authored-by: takoyakimchi <[email protected]> * refactor: 출력 문자열 개행 제거 Co-authored-by: takoyakimchi <[email protected]> * feat: 사다리 연결 여부 Enum 클래스 구현 Co-authored-by: takoyakimchi <[email protected]> * refactor: 사다리 연결 여부 Enum으로 관리 Co-authored-by: takoyakimchi <[email protected]> * test: 사다리 연결 여부 Enum 변경에 맞게 테스트 수정 Co-authored-by: takoyakimchi <[email protected]> * refactor: 에러 처리 prefix 상수 사용 Co-authored-by: takoyakimchi <[email protected]> * refactor: 에러 처리 prefix 상수 사용 Co-authored-by: takoyakimchi <[email protected]> * style: 코드 내 불필요한 개행 제거 Co-authored-by: takoyakimchi <[email protected]> * test: 사다리 연결 여부 Enum화에 따른 테스트 수정 Co-authored-by: takoyakimchi <[email protected]> * docs: 구현된 기능 목록 완료 표시 Co-authored-by: takoyakimchi <[email protected]> * refactor: 이름의 최대 글자 상수 처리 Co-authored-by: takoyakimchi <[email protected]> * style: TODO 목록 제거 Co-authored-by: takoyakimchi <[email protected]> * test: 생성자 테스트 추가, 테스트명 및 메서드명 컨벤션 통일 Co-authored-by: takoyakimchi <[email protected]> * refactor: PointStrategy의 참조 방식 변경 Co-authored-by: takoyakimchi <[email protected]> * refactor: 불필요한 import 제거 Co-authored-by: takoyakimchi <[email protected]> * refactor: String 덧셈 연산 StringBuilder 사용하여 불필요한 객체 생성 과정 제거 * refactor: 사용하고 있지 않은 isConnected 변수 제거 * refactor: 사다리의 연결 상태를 나타내는 클래스 직관적인 이름으로 변경 * refactor: 다음 포인트의 연결 여부를 생성하는 메서드 연결 클래스로 역할 분리 * refactor: 문자열 가공 역할 StringParser 클래스로 분리 * refactor: 컨트롤러의 파라미터를 통해 에러 핸들러 주입 * refactor: 이름 입력 역할과 객체 생성 역할 다른 메서드로 분리 * refactor: Height 객체가 해당 객체를 사용하는 객체 내부에서 생성되도록 수정 및 파싱 로직 책임을 외부로 이전 * style: 불필요한 공백 제거 * refactor: 사다리 연결 여부와 출력 형태 매칭 로직을 리졸버로 이동 * refactor: MessageResolver 클래스 OutputView로 통합 * style: 불필요한 주석 제거 * refactor: 출력되는 문자열 상수로 처리 * refactor: 테스트에서의 재사용을 위해 상수의 접근제어자 변경 * test: 도메인 생성 실패의 경우 에러메시지 검증 로직 추가 * refactor: point와 connection 혼용하던 방식 connection으로 통일 * refactor: 메서드의 길이를 줄이기 위해 추가 메서드로 분리 * test: 절대 연속으로 사다리가 연결될 수 없음을 확인하는 테스트 추가 * refactor: Lines 클래스 정적 팩토리 메서드 도입 * refactor: Members 클래스 정적 팩토리 메서드 도입 * refactor: Line 클래스 정적 팩토리 메서드 도입 --------- Co-authored-by: takoyakimchi <[email protected]> Co-authored-by: takoyakimchi <c4nyou결@gmail.com>
1 parent 6cd50d6 commit 654d675

23 files changed

+708
-4
lines changed

README.md

+22-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,25 @@
1-
# java-ladder
1+
## 기능 요구사항
2+
### 참여자
3+
- [x] 이름은 1글자부터 5글자까지 부여할 수 있다.
4+
- [x] 이름은 쉼표(,)를 기준으로 구분한다.
5+
- [x] 이름은 중복될 수 없다.
6+
- [x] 이름은 알파벳 대소문자 + 숫자로 구성되어야 한다.
7+
- [x] 참여자 수는 최소 2명부터 최대 15명까지이다.
28

3-
사다리 타기 미션 저장소
9+
### 사다리
10+
- [x] 사다리 가로줄의 생성 여부는 랜덤으로 결정된다.
11+
- [x] 사다리 가로줄 라인이 겹치지 않도록 해야 한다.
12+
- [x] `|-----|-----|` 와 같은 모양은 허용하지 않는다.
13+
- [x] 사다리 높이는 1부터 20까지 가능하다.
414

5-
## 우아한테크코스 코드리뷰
15+
### 입력
16+
- [x] 참여할 사람의 이름을 쉼표(,)로 구분하여 입력받는다.
17+
- [x] 최대 사다리 높이를 입력받는다.
18+
- [x] 잘못된 형태로 입력된 경우 다시 입력받는다.
619

7-
- [온라인 코드 리뷰 과정](https://github.com/woowacourse/woowacourse-docs/blob/master/maincourse/README.md)
20+
### 출력
21+
- [x] 사람 이름이 출력되고, 그 다음줄부터 사다리가 출력된다.
22+
- [x] 사람 이름이 5자 미만인 경우 이름의 마지막 글자가 사다리 세로줄의 바로 왼쪽 열에 위치하게 한다.
23+
- [x] 사람 이름이 5자인 경우 이름의 마지막 글자가 사다리 세로줄과 같은 열에 위치하게 한다.
24+
- [x] 사다리 각 행의 가장 왼쪽에는 4칸의 공백이 있다.
25+
- [x] 사다리 세로줄 간의 간격은 5칸으로 한다.

src/main/java/Main.java

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import controller.GameController;
2+
import error.ErrorHandler;
3+
import view.InputView;
4+
import view.OutputView;
5+
6+
public class Main {
7+
8+
public static void main(String[] args) {
9+
InputView inputView = new InputView();
10+
OutputView outputView = new OutputView();
11+
ErrorHandler errorHandler = new ErrorHandler();
12+
13+
GameController gameController = new GameController(inputView, outputView, errorHandler);
14+
gameController.run();
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package controller;
2+
3+
import domain.Game;
4+
import domain.Lines;
5+
import domain.Members;
6+
import domain.StringParser;
7+
import error.ErrorHandler;
8+
import strategy.RandomConnectionStrategy;
9+
import view.InputView;
10+
import view.OutputView;
11+
12+
public class GameController {
13+
14+
private final InputView inputView;
15+
private final OutputView outputView;
16+
private final ErrorHandler errorHandler;
17+
18+
public GameController(InputView inputView, OutputView outputView, ErrorHandler errorHandler) {
19+
this.inputView = inputView;
20+
this.outputView = outputView;
21+
this.errorHandler = errorHandler;
22+
}
23+
24+
public void run() {
25+
26+
Members members = makeMembers();
27+
28+
Lines lines = makeLines(members);
29+
30+
Game game = new Game(members, lines);
31+
outputView.printResult(game);
32+
}
33+
34+
private Members makeMembers() {
35+
return errorHandler.readUntilNoError(() -> {
36+
String rawNames = inputView.readNames();
37+
return Members.from(rawNames);
38+
});
39+
}
40+
41+
private Lines makeLines(Members members) {
42+
return errorHandler.readUntilNoError(() -> {
43+
String rawHeight = inputView.readHeight();
44+
int height = StringParser.stringToInt(rawHeight);
45+
return Lines.of(members.getCount(), height, new RandomConnectionStrategy());
46+
});
47+
}
48+
}

src/main/java/domain/Connection.java

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package domain;
2+
3+
import strategy.ConnectionStrategy;
4+
5+
public enum Connection {
6+
7+
CONNECTED,
8+
DISCONNECTED;
9+
10+
Connection() {
11+
}
12+
13+
public Connection makeNextConnection(ConnectionStrategy connectionStrategy) {
14+
if (this == CONNECTED) {
15+
return DISCONNECTED;
16+
}
17+
return connectionStrategy.generateConnection();
18+
}
19+
}

src/main/java/domain/Game.java

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package domain;
2+
3+
public class Game {
4+
5+
private final Members members;
6+
private final Lines lines;
7+
8+
public Game(Members members, Lines lines) {
9+
this.members = members;
10+
this.lines = lines;
11+
}
12+
13+
public Members getMembers() {
14+
return members;
15+
}
16+
17+
public Lines getLines() {
18+
return lines;
19+
}
20+
}

src/main/java/domain/Height.java

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package domain;
2+
3+
public class Height {
4+
5+
public static final int MIN_HEIGHT = 1;
6+
public static final int MAX_HEIGHT = 20;
7+
8+
private final int value;
9+
10+
public Height(int value) {
11+
validateRange(value);
12+
this.value = value;
13+
}
14+
15+
private void validateRange(int value) {
16+
if (value < MIN_HEIGHT || value > MAX_HEIGHT) {
17+
throw new IllegalArgumentException(MIN_HEIGHT + " 이상 " + MAX_HEIGHT + " 이하의 숫자를 입력해 주세요.");
18+
}
19+
}
20+
21+
public int getValue() {
22+
return value;
23+
}
24+
}

src/main/java/domain/Line.java

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package domain;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
import strategy.ConnectionStrategy;
6+
7+
public class Line {
8+
9+
private final List<Connection> connections;
10+
11+
private Line(List<Connection> connections) {
12+
this.connections = connections;
13+
}
14+
15+
public static Line from(int memberCount, ConnectionStrategy connectionStrategy) {
16+
List<Connection> connections = new ArrayList<>();
17+
connections.add(connectionStrategy.generateConnection());
18+
19+
for (int i = 1; i < memberCount - 1; i++) {
20+
Connection previous = connections.get(i - 1);
21+
Connection next = previous.makeNextConnection(connectionStrategy);
22+
connections.add(next);
23+
}
24+
return new Line(connections);
25+
}
26+
27+
public List<Connection> getConnections() {
28+
return connections;
29+
}
30+
}

src/main/java/domain/Lines.java

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package domain;
2+
3+
import java.util.ArrayList;
4+
import java.util.Collections;
5+
import java.util.List;
6+
import strategy.ConnectionStrategy;
7+
8+
public class Lines {
9+
10+
private final List<Line> lines;
11+
12+
private Lines(List<Line> lines) {
13+
this.lines = lines;
14+
}
15+
16+
public static Lines of(int memberCount, int height, ConnectionStrategy connectionStrategy) {
17+
List<Line> lines = new ArrayList<>();
18+
for (int i = 0; i < height; i++) {
19+
lines.add(Line.from(memberCount, connectionStrategy));
20+
}
21+
return new Lines(lines);
22+
}
23+
24+
public List<Line> getLines() {
25+
return Collections.unmodifiableList(lines);
26+
}
27+
}

src/main/java/domain/Member.java

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package domain;
2+
3+
public class Member {
4+
5+
private final Name name;
6+
7+
public Member(String rawName) {
8+
this.name = new Name(rawName);
9+
}
10+
11+
public String getName() {
12+
return name.getName();
13+
}
14+
}

src/main/java/domain/Members.java

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package domain;
2+
3+
import java.util.ArrayList;
4+
import java.util.HashSet;
5+
import java.util.List;
6+
import java.util.Set;
7+
8+
public class Members {
9+
10+
private static final String DELIMITER = ",";
11+
private static final int MIN_MEMBER_COUNT = 2;
12+
private static final int MAX_MEMBER_COUNT = 15;
13+
14+
private final List<Member> members;
15+
16+
private Members(List<Member> members) {
17+
this.members = members;
18+
}
19+
20+
public static Members from(String rawNames) {
21+
List<String> names = StringParser.splitByDelimiter(rawNames, DELIMITER);
22+
validateDuplication(names);
23+
validateCount(names);
24+
List<Member> members = makeMembers(names);
25+
return new Members(members);
26+
}
27+
28+
private static void validateDuplication(List<String> names) {
29+
Set<String> nonDuplicated = new HashSet<>(names);
30+
if (names.size() != nonDuplicated.size()) {
31+
throw new IllegalArgumentException("이름은 서로 중복될 수 없습니다.");
32+
}
33+
}
34+
35+
private static void validateCount(List<String> names) {
36+
if (names.size() < MIN_MEMBER_COUNT || names.size() > MAX_MEMBER_COUNT) {
37+
throw new IllegalArgumentException("참여자는 " + MIN_MEMBER_COUNT + "~" + MAX_MEMBER_COUNT + "명만 허용됩니다.");
38+
}
39+
}
40+
41+
private static List<Member> makeMembers(List<String> names) {
42+
List<Member> members = new ArrayList<>();
43+
names.forEach(name -> members.add(new Member(name)));
44+
return members;
45+
}
46+
47+
public int getCount() {
48+
return members.size();
49+
}
50+
51+
public List<String> getNames() {
52+
return members.stream()
53+
.map(Member::getName)
54+
.toList();
55+
}
56+
}

src/main/java/domain/Name.java

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package domain;
2+
3+
public class Name {
4+
5+
private static final int MIN_NAME_LENGTH = 1;
6+
private static final int MAX_NAME_LENGTH = 5;
7+
private static final String FORMAT_NAME = "^[A-Za-z0-9]+$";
8+
9+
private final String name;
10+
11+
public Name(String name) {
12+
validate(name);
13+
this.name = name;
14+
}
15+
16+
private void validate(String name) {
17+
validateNull(name);
18+
validateLength(name);
19+
validatePattern(name);
20+
}
21+
22+
private void validateNull(String name) {
23+
if (name == null) {
24+
throw new IllegalArgumentException("이름에 null을 입력할 수 없습니다.");
25+
}
26+
}
27+
28+
private void validateLength(String name) {
29+
if (name.length() < MIN_NAME_LENGTH || name.length() > MAX_NAME_LENGTH) {
30+
throw new IllegalArgumentException(MIN_NAME_LENGTH + "~" + MAX_NAME_LENGTH + "자의 이름만 허용합니다.");
31+
}
32+
}
33+
34+
private void validatePattern(String name) {
35+
if (!name.matches(FORMAT_NAME)) {
36+
throw new IllegalArgumentException("이름은 알파벳과 숫자만 허용합니다.");
37+
}
38+
}
39+
40+
public String getName() {
41+
return name;
42+
}
43+
}
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package domain;
2+
3+
import java.util.Arrays;
4+
import java.util.List;
5+
6+
public class StringParser {
7+
8+
public static List<String> splitByDelimiter(String before, String delimiter) {
9+
try {
10+
return Arrays.stream(before.split(delimiter, -1))
11+
.map(String::trim)
12+
.toList();
13+
} catch (NullPointerException e) {
14+
throw new IllegalArgumentException("null을 입력할 수 없습니다.");
15+
}
16+
}
17+
18+
public static int stringToInt(String before) {
19+
try {
20+
return Integer.parseInt(before);
21+
} catch (NumberFormatException e) {
22+
throw new IllegalArgumentException("숫자를 입력해 주세요.");
23+
}
24+
}
25+
}

src/main/java/error/ErrorHandler.java

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package error;
2+
3+
import java.util.function.Supplier;
4+
5+
public class ErrorHandler {
6+
7+
private static final String ERROR_PREFIX = "[ERROR] ";
8+
9+
public <T> T readUntilNoError(Supplier<T> supplier) {
10+
try {
11+
return supplier.get();
12+
} catch (IllegalArgumentException e) {
13+
System.out.println(ERROR_PREFIX + e.getMessage());
14+
return readUntilNoError(supplier);
15+
}
16+
}
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package strategy;
2+
3+
import domain.Connection;
4+
5+
public interface ConnectionStrategy {
6+
7+
Connection generateConnection();
8+
}

0 commit comments

Comments
 (0)