Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
43952e6
docs: 요구사항 모델링 및 정의
kangrae-jo Jan 5, 2026
425c279
feat: 레벨과 과정 enum 구현
kangrae-jo Jan 5, 2026
8f1a962
feat: 파일 입력 기능 구현
kangrae-jo Jan 5, 2026
6a7478d
feat: 크루 클래스 구현
kangrae-jo Jan 5, 2026
ccd4c4f
feat: 기본 클래스 구현
kangrae-jo Jan 5, 2026
c2cedce
feat: 크루 객체 생성 기능 추가
kangrae-jo Jan 5, 2026
6d4ab31
feat: 미션 enum 추가
kangrae-jo Jan 5, 2026
0b7875a
feat: 미션의 레벨별 출력 메서드 구현
kangrae-jo Jan 5, 2026
cfb2302
feat: 과정, 레벨, 미션을 입력받는 기능 구현
kangrae-jo Jan 5, 2026
c0ba8be
feat: 페어 매칭 구현
kangrae-jo Jan 5, 2026
0973b1f
fix: 페어 매칭 입력 방식 변경
kangrae-jo Jan 5, 2026
4128065
feat: 페어 초기화 기능 구현
kangrae-jo Jan 5, 2026
84b8784
feat: 과정 레벨 미션을 선택받는 기능 구현
kangrae-jo Jan 5, 2026
2107ee0
refactor: 사용하지않는 메서드 삭제
kangrae-jo Jan 5, 2026
5b4e712
refactor: course별로 인원을 담도록 Crews 수정
kangrae-jo Jan 5, 2026
09972c7
feat: 페어를 구하는 기능 구현
kangrae-jo Jan 5, 2026
1b13a95
feat: 이미 매칭된 결과가 있는지 확인하는 기능 구현
kangrae-jo Jan 5, 2026
08bf621
feat: 결과를 저장하는 로직 구현
kangrae-jo Jan 5, 2026
ca4ffe8
feat: 미션이 레벨을 포함하도록 변경
kangrae-jo Jan 5, 2026
1533617
fix: 레벨별로 다른 미션이 포함되도록 변경
kangrae-jo Jan 5, 2026
c5b0856
feat: 페어 매칭 구현
kangrae-jo Jan 5, 2026
2522c0e
feat: 조회기능 구현
kangrae-jo Jan 5, 2026
05116eb
fix: 조회 기능 오류 수정
kangrae-jo Jan 5, 2026
2422809
fix: 매칭 기능도 조회가 되도록 수정
kangrae-jo Jan 5, 2026
e6bbfe4
fix: 페어 매칭 null point 오류 수정
kangrae-jo Jan 5, 2026
a95e0e4
feat: 재입력 로직 구현
kangrae-jo Jan 5, 2026
4f2659d
fix: 홀수명 course crew에서 null point 접근하지 않도록 변경
kangrae-jo Jan 5, 2026
eabf087
feat: 초기화 문구 추가
kangrae-jo Jan 5, 2026
f248a3f
feat: 잘못된 네 Or 아니오 문구 재입력 로직 구현
kangrae-jo Jan 5, 2026
f2f9130
fix: 다시 매칭하지 않을 때 과정 레벨 미션을 선택하는 곳에서 재입력 받는 로직 구현
kangrae-jo Jan 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 202 additions & 0 deletions SOLUTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
# 페어 매칭 관리 모델링

## 용어 정리

### 크루
| 용어 | 설명 |
|-------------|----------------|
| Crew | 우테코에서 학습하는 사람들 |
| Crew Name | 크루 이름 |
| Crew Course | 크루가 참여하는 분야 |

### 미션
| 용어 | 설명 |
|---------|-----------------------|
| Mission | 학습 레벨마다 구분되어 존재하는 미션들 |

### 레벨
| 용어 | 설명 |
|---------|------------------------|
| Level | 우테코 학습 단계를 구분한 것 1~5단계 존재 |
| Mission | 각 단계마다 미션이 존재 |

### 분야
| 용어 | 설명 |
|--------|----------------------------|
| Course | 크루가 참가하는 분야로 백엔드 프론트엔드가 있음 |


## 개발 요구사항

### 페어 매칭 기능
- 페어 매칭 조건
- [ ] 미션을 함께 수행할 페어를 두명씩 매칭한다.
- [ ] 페어 매칭 대상이 홀수인 경우 한 페어는 3인으로 구성한다.
- [ ] 같은 레벨에서 이미 페어를 맺은 크루와는 다시 페어로 매칭될 수 없다.

- 페어 매칭 구현 방법
- [ ] 크루들의 이름 목록을 List<String> 형태로 준비한다.
- [ ] 크루 목록의 순서를 랜덤으로 섞는다. 이 때 `camp.nextstep.edu.missionutils.Randoms`의 shuffle 메서드를 활용해야 한다.
- [ ] 랜덤으로 섞인 페어 목록에서 페어 매칭을 할 때 앞에서부터 순서대로 두명씩 페어를 맺는다.
- [ ] 홀수인 경우 마지막 남은 크루는 마지막 페어에 포함시킨다.
- [ ] 같은 레벨에서 이미 페어로 만난적이 있는 크루끼리 다시 페어로 매칭 된다면 크루 목록의 순서를 다시 랜덤으로 섞어서 매칭을 시도한다.
- [ ] 3회 시도까지 매칭이 되지 않거나 매칭을 할 수 있는 경우의 수가 없으면 에러 메시지를 출력한다.

### 파일 입출력
- [ ] 페어 매칭에 필요한 크루들의 이름을 파일 입출력을 통해 불러온다.
- [ ] `src/main/resources/backend-crew.md`과 `src/main/resources/frontend-crew.md` 파일을 이용한다.
- [ ] 두 파일의 내용은 수정이 가능하다. 수정 시 크루들의 이름은 중복될 수 없다.
- [ ] 파일 입출력 방법은 `자바 파일 읽기`나 `자바 파일 입출력`과 같은 키워드로 구글링해서 찾을 수 있다.

### 에러 처리
- [ ] 사용자가 잘못된 값을 입력할 경우 `IllegalArgumentException`를 발생시키고, `[ERROR]`로 시작하는 에러 메시지를 출력 후 해당 부분부터 다시 입력을 받는다.
- [ ] 아래의 프로그래밍 실행 결과 예시와 동일하게 입력과 출력이 이루어져야 한다.

### 기능 선택
- [ ] 프로그램을 시작하면 기능의 종류를 출력하고 그 중 하나의 입력을 받는다.

```
기능을 선택하세요.
1. 페어 매칭
2. 페어 조회
3. 페어 초기화
Q. 종료
```

### 페어 매칭
- [ ] 과정와 미션을 출력하고 매칭하고자 하는 과정, 레벨, 미션을 입력 받는다.

```
#############################################
과정: 백엔드 | 프론트엔드
미션:
- 레벨1: 자동차경주 | 로또 | 숫자야구게임
- 레벨2: 장바구니 | 결제 | 지하철노선도
- 레벨3:
- 레벨4: 성능개선 | 배포
- 레벨5:
############################################
과정, 레벨, 미션을 선택하세요.
ex) 백엔드, 레벨1, 자동차경주
```

- [ ] 매칭이 정상적으로 수행되면 결과가 출력된다.
- [ ] 출력되는 페어의 순서는 `camp.nextstep.edu.missionutils.Randoms`의 shuffle 메서드의 결과 순서로 정렬한다.

### 프로그래밍 실행 결과 예시

```
기능을 선택하세요.
1. 페어 매칭
2. 페어 조회
3. 페어 초기화
Q. 종료
1

#############################################
과정: 백엔드 | 프론트엔드
미션:
- 레벨1: 자동차경주 | 로또 | 숫자야구게임
- 레벨2: 장바구니 | 결제 | 지하철노선도
- 레벨3:
- 레벨4: 성능개선 | 배포
- 레벨5:
############################################
과정, 레벨, 미션을 선택하세요.
ex) 백엔드, 레벨1, 자동차경주
프론트엔드, 레벨1, 자동차경주

페어 매칭 결과입니다.
다비 : 신디
쉐리 : 덴버
제키 : 로드
라라 : 윌터
니콜 : 이브
린다 : 시저
보노 : 제시 : 제키

기능을 선택하세요.
1. 페어 매칭
2. 페어 조회
3. 페어 초기화
Q. 종료
1

#############################################
과정: 백엔드 | 프론트엔드
미션:
- 레벨1: 자동차경주 | 로또 | 숫자야구게임
- 레벨2: 장바구니 | 결제 | 지하철노선도
- 레벨3:
- 레벨4: 성능개선 | 배포
- 레벨5:
############################################
과정, 레벨, 미션을 선택하세요.
ex) 백엔드, 레벨1, 자동차경주
프론트엔드, 레벨1, 자동차경주

매칭 정보가 있습니다. 다시 매칭하시겠습니까?
네 | 아니오
아니오

과정, 레벨, 미션을 선택하세요.
ex) 백엔드, 레벨1, 자동차경주
프론트엔드, 레벨1, 자동차경주
매칭 정보가 있습니다. 다시 매칭하시겠습니까?
네 | 아니오

페어 매칭 결과입니다.
이브 : 윌터
보노 : 제키
신디 : 로드
제시 : 린다
시저 : 라라
니콜 : 다비
리사 : 덴버 : 제키

기능을 선택하세요.
1. 페어 매칭
2. 페어 조회
3. 페어 초기화
Q. 종료
2

#############################################
과정: 백엔드 | 프론트엔드
미션:
- 레벨1: 자동차경주 | 로또 | 숫자야구게임
- 레벨2: 장바구니 | 결제 | 지하철노선도
- 레벨3:
- 레벨4: 성능개선 | 배포
- 레벨5:
############################################
과정, 레벨, 미션을 선택하세요.
ex) 백엔드, 레벨1, 자동차경주
프론트엔드, 레벨1, 자동차경주

페어 매칭 결과입니다.
이브 : 윌터
보노 : 제키
신디 : 로드
제시 : 린다
시저 : 라라
니콜 : 다비
리사 : 덴버 : 제키

기능을 선택하세요.
1. 페어 매칭
2. 페어 조회
3. 페어 초기화
Q. 종료
3

초기화 되었습니다.

기능을 선택하세요.
1. 페어 매칭
2. 페어 조회
3. 페어 초기화
Q. 종료
Q
```
9 changes: 8 additions & 1 deletion src/main/java/pairmatching/Application.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
package pairmatching;

import pairmatching.config.AppConfig;
import pairmatching.controller.Controller;

public class Application {

public static void main(String[] args) {
// TODO 구현 진행
AppConfig appConfig = new AppConfig();
Controller controller = appConfig.controller();
controller.run();
}

}
37 changes: 37 additions & 0 deletions src/main/java/pairmatching/config/AppConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package pairmatching.config;

import pairmatching.controller.Controller;
import pairmatching.view.InputView;
import pairmatching.view.OutputView;

public class AppConfig {

private InputView inputView;
private OutputView outputView;
private Controller controller;

public AppConfig() {
}

private InputView inputView() {
if (inputView == null) {
return inputView = new InputView();
}
return inputView;
}

private OutputView outputView() {
if (outputView == null) {
return outputView = new OutputView();
}
return outputView;
}

public Controller controller() {
if (controller == null) {
return controller = new Controller(inputView(), outputView());
}
return controller;
}

}
127 changes: 127 additions & 0 deletions src/main/java/pairmatching/controller/Controller.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package pairmatching.controller;

import static pairmatching.service.FileReaderService.BACKEND_CREW_DIR;
import static pairmatching.service.FileReaderService.FRONTEND_CREW_DIR;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
import pairmatching.model.Course;
import pairmatching.model.Crew;
import pairmatching.model.Crews;
import pairmatching.model.InputInformation;
import pairmatching.model.Result;
import pairmatching.service.FileReaderService;
import pairmatching.service.InputParser;
import pairmatching.view.InputView;
import pairmatching.view.OutputView;

public class Controller {

private final InputView inputView;
private final OutputView outputView;

public Controller(InputView inputView, OutputView outputView) {
this.inputView = inputView;
this.outputView = outputView;
}

public void run() {
List<Crew> backendCrews = readCrews(BACKEND_CREW_DIR, Course.BACKEND);
List<Crew> frontendCrews = readCrews(FRONTEND_CREW_DIR, Course.FRONTEND);
Crews crews = new Crews(backendCrews, frontendCrews);

Result result = Result.init();
String function;
while (!"Q".equals(function = inputView.readFunctionSelect())) {
if ("3".equals(function)) {
result = Result.init();
outputView.printInitMsg();
continue;
}

if ("1".equals(function)) {
handlePairMatchingProcess(result, crews);
}
if ("2".equals(function)) {
InputInformation courseAndLevelAndMission = readCourseAndLevelAndMission();
handlePrintPairMatching(result, courseAndLevelAndMission);
}
}
}

private List<Crew> readCrews(String dir, Course course) {
return retryUntilValid(() -> {
List<String> names = FileReaderService.readFile(dir);
List<Crew> crews = new ArrayList<>();
for (String name : names) {
crews.add(Crew.from(course, name));
}
return crews;
});
}

private InputInformation readCourseAndLevelAndMission() {
return retryUntilValid(() ->
InputParser.parseCourseAndLevelAndMission(inputView.readCourseAndLevelAndMission())
);
}

private Object handlePairMatchingProcess(Result result, Crews crews) {
return retryUntilValid(() -> {
InputInformation courseAndLevelAndMission = readCourseAndLevelAndMission();
handlePairMatching(result, courseAndLevelAndMission, crews);
handlePrintPairMatching(result, courseAndLevelAndMission);
return null;
});
}

private void handlePairMatching(Result result, InputInformation information, Crews crews) {
// 이미 생성된 정보가 있을 때
if (result.isExist(information)) {
String rematching = readRematching();
if ("아니오".equals(rematching)) {
throw new IllegalStateException();
}
}

int retry = 0;
while (retry < 3) {
try {
// 정보 생성 및 저장
List<List<String>> pairs = crews.pairMatching(information.getCourse());
result.put(information, pairs);
// 같은 레벨에서 같은 크루가 두 번 이상 만나는지 검사
result.isAlreadyMatched();
return;
} catch (IllegalArgumentException e) {
retry++;
}
}
throw new IllegalArgumentException("3회 시도까지 매칭이 되지 않았습니다.");
}

private String readRematching() {
return retryUntilValid(() ->
InputParser.parseYesOrNo(inputView.readRematching())
);
}

private void handlePrintPairMatching(Result result, InputInformation courseAndLevelAndMission) {
List<List<String>> pairs = result.getPairs(courseAndLevelAndMission);
outputView.printPairMatching(pairs);
}

private <T> T retryUntilValid(Supplier<T> supplier) {
while (true) {
try {
return supplier.get();
} catch (IllegalArgumentException e) {
outputView.printErrorMsg(e.getMessage());
} catch (IllegalStateException e) {
continue;
}
}
}

}
Loading