Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
ad5c897
docs: 페어 매칭
inpink Nov 27, 2023
6d7874f
docs: 페어 조회, 초기화
inpink Nov 27, 2023
9ffa133
docs: 사전 정보, 시작 화면
inpink Nov 27, 2023
d7855f3
docs: 입력 화면
inpink Nov 27, 2023
1a26d7a
docs: 커밋 컨벤션
inpink Nov 27, 2023
00939ad
test: 페어 매칭에 필요한 크루들의 이름을 파일 입출력을 통해 불러옴
inpink Nov 27, 2023
a56846d
feat: 페어 매칭에 필요한 크루들의 이름을 파일 입출력을 통해 불러옴
inpink Nov 27, 2023
84af92f
feat: 백엔드, 프론트엔드 Course
inpink Nov 27, 2023
30137eb
docs: 출력 화면
inpink Nov 27, 2023
51eb797
feat: 사전 제공 정보 enum으로 구현
inpink Nov 27, 2023
c8b9e13
feat: 사용자는 "기능 메뉴 선택" 정수 1개를 입력
inpink Nov 27, 2023
8387cd1
feat: 기능 메뉴
inpink Nov 27, 2023
4075c7e
feat: 사용자는 "과정, 레벨, 미션"을 ,로 구분하여 한 줄에 입력
inpink Nov 27, 2023
c3c20c9
test: 사용자는 "과정, 레벨, 미션"을 ,로 구분하여 한 줄에 입력
inpink Nov 27, 2023
95981e3
refactor: 사용자는 "과정, 레벨, 미션"을 ,로 구분하여 한 줄에 입력
inpink Nov 27, 2023
f7420a4
feat: 없는 "과정, 레벨, 미션"을 입력 시 예외 처리
inpink Nov 27, 2023
16604fc
refactor: Mission을 String 대신 VO 사용
inpink Nov 27, 2023
ea9fe48
refactor: 패키지 구조 이동
inpink Nov 27, 2023
a01b46c
feat: 과정와 미션을 출력
inpink Nov 27, 2023
d3be825
test: Course의 path로 crew List 불러오기
inpink Nov 27, 2023
4051f41
feat: Course의 path로 crew List 불러오기
inpink Nov 27, 2023
464abd8
test: CourseName으로 Course 객체 찾기
inpink Nov 27, 2023
386b73c
refactor: 기능 목록 출력
inpink Nov 27, 2023
2fa1c6b
refactor: 에러 메세지 출력
inpink Nov 27, 2023
f4130e3
feat: 사용자가 잘못된 값을 입력하면 IllegalArgumentException와 [ERROR]로 시작하는 에러 메시지…
inpink Nov 27, 2023
a2eac25
test: 페어 매칭 결과 변환
inpink Nov 27, 2023
22e2c6a
feat: 페어 매칭 결과 변환
inpink Nov 27, 2023
08d1d95
refactor: util성 클래스에 private 생성자
inpink Nov 27, 2023
5d223d2
feat: 크루 목록의 순서를 랜덤으로 섞는다.
inpink Nov 27, 2023
c554794
feat: 페어 매칭 결과 출력
inpink Nov 27, 2023
4a83a2b
refactor: 정적 팩터리 메서드 적용
inpink Nov 27, 2023
26b2dcb
fix: 오타 수정
inpink Nov 27, 2023
60b14ef
feat: 저장소에 저장
inpink Nov 27, 2023
be11956
feat: 매칭 이력이 없으면 매칭 이력이 없다고 안내한다
inpink Nov 27, 2023
2a0ac29
feat: 이미 매칭 정보가 있는 경우 재매칭할지에 대한 정보를 입력받는다.
inpink Nov 27, 2023
10c7a42
feat: 홀수인 경우 마지막 남은 크루는 마지막 페어에 포함시킨다.
inpink Nov 27, 2023
284d263
feat: 3회 시도까지 매칭이 되지 않거나 매칭을 할 수 있는 경우의 수가 없으면 에러 메시지를 출력한다
inpink Nov 27, 2023
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
121 changes: 121 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
[작성자] 
이름: 양홍주
메일: [email protected] 


## 구현할 "기능 목록"을 정리

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


[페어 조회 기능]
- 과정, 레벨, 미션을 선택하면 해당 미션의 페어 정보를 출력한다. O
- 매칭 이력이 없으면 매칭 이력이 없다고 안내한다. O
- 이 때, 에러 메세지는 다음과 같다. "[ERROR] 매칭 이력이 없습니다." O


[페어 초기화 기능]
- 페어 매칭 정보를 모두 초기화시킨다. O

[사전 제공 정보]
- 과정 O
- 백엔드 O
- 프론트엔드 O
- 레벨 O
- 레벨1 O
- 레벨2 O
- 레벨3 O
- 레벨4 O
- 레벨5 O
- 미션 O
- 레벨1 O
- 자동차경주 O
- 로또 O
- 숫자야구게임 O
- 레벨2 O
- 장바구니 O
- 결제 O
- 지하철노선도 O
- 레벨3 (페어메칭 없음) O
- 레벨4 O
- 성능개선 O
- 배포 O
- 레벨5 (페어메칭 없음) O

- 크루 정보는 src/resources 하위에 md 파일로 제공된다. O
- 백엔드, 프론트엔드 각각 파일이 존재한다. O


[입력 화면]
- 사용자가 잘못된 값을 입력하면 IllegalArgumentException와 [ERROR]로 시작하는 에러 메시지를 출력 후 해당 부분부터 다시 입력을 받는다. O
- 사용자는 "기능 메뉴 선택" 정수 1개를 입력 O
- 1번은 페어 매칭, 2번은 페어 조회, 3번은 페어 초기화, Q는 종료 O
- 검증: 만약 없는 메뉴를 입력하면 잘못 입력한 것. O
- 오타 교정: 앞뒤로 공백을 제거해줌 O
- 사용자는 "과정, 레벨, 미션"을 ,로 구분하여 한 줄에 입력 O
- 검증: 없는 과정을 입력하면 잘못된 입력 O
- 검증: 없는 레벨을 입력하면 잘못된 입력 O
- 검증: 페어매칭이 없는 과정을 입력하면 잘못된 입력 O
- 검증: 선택한 과정에 입력한 미션이 없으면 잘못된 입력 O
- 오타 교정: 입력 항목 사이에 불필요한 ,는 자동으로 제거해줌 O
- 오타 교정: 전체 입력 문장에서 모든 공백을 제거해줌. O
- 이미 매칭 정보가 있는 경우 재매칭할지에 대한 정보를 입력받는다. O
- 사용자는 문자 하나를 입력한다. O
- 예를 선택할 경우 페어를 재매칭한다. O
- 아니오를 선택할 경우 코스, 레벨, 미션을 다시 선택한다. O
- 검증: 예, 아니오가 아닌 값을 입력하면 잘못입력한 것. O

[출력 화면]
- 과정와 미션을 출력 O
```
#############################################
과정: 백엔드 | 프론트엔드
미션:
- 레벨1: 자동차경주 | 로또 | 숫자야구게임
- 레벨2: 장바구니 | 결제 | 지하철노선도
- 레벨3:
- 레벨4: 성능개선 | 배포
- 레벨5:
############################################
```
- 페어 매칭 결과 출력 O
```
페어 매칭 결과입니다.
이브 : 윌터
보노 : 제키
신디 : 로드
제시 : 린다
시저 : 라라
니콜 : 다비
리사 : 덴버 : 제키
```


## 커밋 컨벤션

> 기본 형태
~~~
type: subject

body
~~~

> type

feat: 새로운 기능 
fix: 버그 수정 
docs: 문서 수정 
style: 서식 지정, 세미콜론 누락 등 (코드 변경 없음) 
refactor: 코드 리팩토링 
test: 테스트 코드 추가, 테스트 코드 리팩토링 (생산 코드 변경 없음) 
chore: 빌드, 패키지 매니저 등 업데이트 (생산 코드 변경 없음) 

14 changes: 13 additions & 1 deletion src/main/java/pairmatching/Application.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
package pairmatching;

import pairmatching.controller.MatchingController;
import pairmatching.repository.MatchingResultRepository;
import pairmatching.service.MatchingService;
import pairmatching.view.InputView;
import pairmatching.view.OutputView;

public class Application {
public static void main(String[] args) {
// TODO 구현 진행
MatchingController matchingController = new MatchingController(
new InputView(),
new OutputView(),
new MatchingService(new MatchingResultRepository())
);

matchingController.play();
}
}
166 changes: 166 additions & 0 deletions src/main/java/pairmatching/controller/MatchingController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package pairmatching.controller;

import static pairmatching.domain.entity.Function.PAIR_CHECK;
import static pairmatching.domain.entity.Function.PAIR_MATCHING;
import static pairmatching.domain.entity.Function.PAIR_RESET;
import static pairmatching.domain.entity.Function.QUIT;
import static pairmatching.domain.entity.RematchingOption.YES;
import static pairmatching.messages.ErrorMessages.FAIL_TO_MATCH;
import static pairmatching.messages.ErrorMessages.INVALID_COURSE_MISSION;
import static pairmatching.messages.ErrorMessages.INVALID_FUNCTION;
import static pairmatching.messages.ErrorMessages.INVALID_REMATCHING_SELECT;
import static pairmatching.messages.ErrorMessages.NOT_EXIST_MATCHING_RESULT;

import java.util.Optional;
import pairmatching.domain.dto.MatchingResultMapper;
import pairmatching.domain.entity.Course;
import pairmatching.domain.entity.CourseMission;
import pairmatching.domain.entity.Function;
import pairmatching.domain.entity.MatchingResult;
import pairmatching.domain.entity.RematchingOption;
import pairmatching.service.MatchingService;
import pairmatching.util.InputUtil;
import pairmatching.view.InputView;
import pairmatching.view.OutputView;

public class MatchingController {

private final int REMATCHING_COUNT = 3;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이런 부분에 대해서 저도 고민해봤는데,
3회 체크를 하려면 어쩔수 없이 필드에 값이 존재해야할 것 같았습니다.
하지만 이런 값을을 최대한 배제하려 해서 사용하지 않았는데 이부분에 대해선 어떻게 생각하시나요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

시간이 없어서 분리하지 못했는데..ㅎㅎ 시간 여유가 있었다면 따로 enum에서 관리하는 방법으로 리팩터링 했을 것입니다!


private final InputView inputView;
private final OutputView outputView;
private final MatchingService matchingService;

public MatchingController(InputView inputView, OutputView outputView, MatchingService matchingService) {
this.inputView = inputView;
this.outputView = outputView;
this.matchingService = matchingService;
}

public void play() {

while (true) {
final Function function = inputValidFunction();

if (function.equals(QUIT)) {
return;
}
processFunctionInput(function);
}
}

private void processFunctionInput(Function function) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기서 분기처리를 해주는 것은 어떤가요?

if(function.is~()) {
    ~~~();
    return;
}

이런식으로요!

지금 방식은 모든 메서드에 다 들어가야해서 가독성이 떨어질 것 같습니다!

processPairMatching(function);
processPairCheck(function);
processPairReset(function);
}

private void processPairReset(Function function) {
if (function.equals(PAIR_RESET)) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

취향 차이일수 있는데 저는 function에 직접 물어보는 것이 좋다고 생각합니다.
function.isReset() 이런 식으로요!

matchingService.resetMatching();
outputView.ouputReset();
}
}

private void processPairCheck(Function function) {
if (function.equals(PAIR_CHECK)) {
outputView.outputCourseMission();
final CourseMission courseMission = inputValidCourseMission();

checkMatchingResult(courseMission);
}
}

private void checkMatchingResult(CourseMission courseMission) {
Optional<MatchingResult> matchingResult = matchingService.findMatchingResult(courseMission);

if (!matchingResult.isPresent()) {
outputView.outputErrorMessage(NOT_EXIST_MATCHING_RESULT.getMessage());
return;
}
outputView.outputPairMatchingResult(MatchingResultMapper.from(matchingResult.get()));
}

private void processPairMatching(Function function) {
if (function.equals(PAIR_MATCHING)) {
outputView.outputCourseMission();
final CourseMission courseMission = inputValidCourseMission();
Course course = courseMission.getCourse();

processExistResult(courseMission, course);
}
}

private void processExistResult(CourseMission courseMission, Course course) {
Optional<MatchingResult> foundMatchingResult = matchingService.findMatchingResult(courseMission);

if (foundMatchingResult.isPresent()) {
RematchingOption rematchingOption = inputValidRematchingOption();
processRematching(rematchingOption, course, courseMission);
return;
}
processMatchingResult(course, courseMission);
}

private void processRematching(RematchingOption rematchingOption, Course course, CourseMission courseMission) {
if (rematchingOption.equals(YES)) {
processMatchingResult(course, courseMission);
}
}
Comment on lines +105 to +109
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아니오를 선택하면 코스,레벨,미션을 다시 고르도록 해야하는데,
다시 처음으로 돌아가는 것 같습니다!


private void processMatchingResult(Course course, CourseMission courseMission) {
Optional<MatchingResult> nonDuplicatedResult = createNonDuplicatedResult(courseMission, course);

if (!nonDuplicatedResult.isPresent()) {
outputView.outputErrorMessage(FAIL_TO_MATCH.getMessage());
}

processNonDuplicatedResult(courseMission, nonDuplicatedResult);
}

private void processNonDuplicatedResult(CourseMission courseMission, Optional<MatchingResult> nonDuplicatedResult) {
MatchingResult matchingResult = nonDuplicatedResult.get();
outputView.outputPairMatchingResult(MatchingResultMapper.from(matchingResult));
matchingService.save(courseMission, matchingResult);
}
Comment on lines +121 to +125
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional 처리를 service나 repository에서 하는 것이 좋을 것 같습니다!
정확히는 repository에서 Optional을 벗겨서 오는 것이 좋지 않을까 생각합니다!

근거로 JPA Repository의 getById 메서드를 들려고 했는데 Deprecated 됐네요...
그래도 .orElseThrow 같은걸 사용해서 컨트롤러에는 깔끔한 객체를 보여주는 것은 어떤가요?
코틀린처럼 Elvis 연산자가 있으면 좋을텐데 ㅠㅠㅠ


private Optional<MatchingResult> createNonDuplicatedResult(CourseMission courseMission, Course course) {
int count = 0;

while (count < REMATCHING_COUNT) {
MatchingResult matchingResult = matchingService.createMatchingResult(course.getCrews());
if (!matchingService.isDuplicatedPair(courseMission,matchingResult)) {
return Optional.ofNullable(matchingResult);
}
}

return Optional.empty();
}

private RematchingOption inputValidRematchingOption() {
return InputUtil.retryOnInvalidInput(this::inputRematchingOption,
errorMessage -> outputView.outputErrorMessage(INVALID_REMATCHING_SELECT.getMessage()));
}

private RematchingOption inputRematchingOption() {
return inputView.inputRematchingOption();
}

private CourseMission inputValidCourseMission() {
return InputUtil.retryOnInvalidInput(this::inputCourseMission,
errorMessage -> outputView.outputErrorMessage(INVALID_COURSE_MISSION.getMessage()));
}

private CourseMission inputCourseMission() {
return inputView.inputCourseMission();
}

private Function inputValidFunction() {
return InputUtil.retryOnInvalidInput(this::inputFunction,
errorMessage -> outputView.outputErrorMessage(INVALID_FUNCTION.getMessage()));
}

private Function inputFunction() {
return inputView.inputFunction();
}
Comment on lines +158 to +165
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

private Function inputValidFunction() {
        return InputUtil.retryOnInvalidInput(inputView::inputFunction,
                errorMessage -> outputView.outputErrorMessage(INVALID_FUNCTION.getMessage()));
    }

진짜 큰 차이 아니긴 한데 이렇게 수정하는건 어떤가요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

훨씬 간단해지고 좋네요!! 감사합니다 :)

}
50 changes: 50 additions & 0 deletions src/main/java/pairmatching/domain/dto/CourseMissionMapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package pairmatching.domain.dto;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import pairmatching.domain.entity.Course;
import pairmatching.domain.entity.CourseMission;
import pairmatching.domain.entity.Level;
import pairmatching.domain.entity.Mission;
import pairmatching.util.StringUtil;

public class CourseMissionMapper {

private CourseMissionMapper() {

}

public static CourseMission toCourseMission(String input) {
List<String> separated = seperate(input);

final String courseName = separated.get(0);
final String levelName = separated.get(1);
final String missionName = separated.get(2);

Course course = toCourse(courseName);
Level level = toLevel(levelName);
Mission mission = toMission(level, missionName);

return CourseMission.create(course, level, mission);
}

private static List<String> seperate(String input) {
String deleteSpaces = StringUtil.removeAllSpaces(input);
List<String> separated = Arrays.stream(deleteSpaces.split(","))
.collect(Collectors.toList()); //실전에선 toList() 사용
return separated;
}

private static Level toLevel(String levelName) {
return Level.findLevel(levelName);
}

private static Course toCourse(String courseName) {
return Course.findCourse(courseName);
}

private static Mission toMission(Level level, String missionName) {
return level.findMission(missionName);
}
}
16 changes: 16 additions & 0 deletions src/main/java/pairmatching/domain/dto/FunctionMapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package pairmatching.domain.dto;

import pairmatching.domain.entity.Function;
import pairmatching.util.StringUtil;

public class FunctionMapper {
private FunctionMapper() {

}

public static Function toFunction(String input) {
final String deleteSpaces = StringUtil.removeAllSpaces(input);

return Function.findFunction(deleteSpaces);
}
}
17 changes: 17 additions & 0 deletions src/main/java/pairmatching/domain/dto/MatchingResultDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package pairmatching.domain.dto;

import java.util.List;

public class MatchingResultDto {

private final List<List<String>> matchingResult;

public MatchingResultDto(List<List<String>> matchingResult) {
this.matchingResult = matchingResult;
}

public List<List<String>> getMatchingResult() {
return matchingResult;
}
}

Loading