diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..f8bc196c0 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,47 @@ +## 사전 제공 정보 저장 + +크루 정보 저장 +크루 정보가 저장된 XML파일에서 크루 이름 목록을 불러온다. +List으로 변환하여 저장한다. +과정, 레벨, 미션 정보 저장 +과정, 레벨, 미션을 Enum class에서 정의한다. +정보를 그룹화하여 저장한다. + +## 실행할 기능을 선택하는 기능 + +`기능을 선택하세요.`를 출력한다. +`1. 페어 매칭 2. 페어 조회 3. 페어 초기화 Q. 종료`를 출력한다. +실행할 기능을 입력받는다. +1, 2, 3, Q에 해당하는 지 검증한다. + +## 페어 매칭 기능 + +과정, 레벨, 미션 정보를 출력한다. +`과정, 레벨, 미션을 선택하세요.\nex) 백엔드, 레벨1, 자동차경주`를 출력한다. +`프론트엔드, 레벨1, 자동차경주` 형식으로 입력 받는다. +빈 문자열인 경우를 검증한다. +올바른 구분자로 입력되었음을 검증한다. +과정이 백엔드 혹은 프론트엔드 임을 검증한다. +레벨이 레벨1, 레벨2, 레벨3, 레벨4, 레벨5 중 하나임을 검증한다. +미션이 존재하는 미션임을 검증한다. +이미 매칭 정보가 있는 경우 +`매칭 정보가 있습니다. 다시 매칭하시겠습니까?`를 출력한다. +`네` 입력 시 매칭 후 결과를 다시 저장한다. +`아니오` 입력 시 코스, 레벨, 미션을 다시 선택한다. +네/아니오 이외의 입력 시 예외를 발생한다. +크루 목록을 랜덤으로 섞는다. +앞에서부터 두 명씩 페어를 묶는다. +만약, 같은 레벨에서 만난 페어가 있다면 다시 섞고 매칭한다. (3회 반복) +같은 레벨에 있는 모든 미션의 매칭 정보를 불러온다. +이미 매칭된 바가 있는 페어라면 재시도한다. +3회 시도까지 매칭이 되지 않았다면 에러 메시지를 출력한다. +해당 레벨에서 짝지어진 페어의 목록을 저장한다. + +## 페어 조회 기능 + +과정, 레벨, 미션을 선택하면 해당 미션의 페어 정보를 출력한다. +매칭 이력이 없으면 `[ERROR] 매칭 이력이 없습니다.`를 출력한다. + +## 페어 초기화 기능 + +모든 미션에 대한 페어 매칭 정보를 초기화한다. diff --git a/src/main/java/pairmatching/Application.java b/src/main/java/pairmatching/Application.java index 6f56e741c..c8e43bfd7 100644 --- a/src/main/java/pairmatching/Application.java +++ b/src/main/java/pairmatching/Application.java @@ -1,7 +1,22 @@ package pairmatching; +import org.xml.sax.SAXException; +import pairmatching.controller.PairController; +import pairmatching.domain.Matching; +import pairmatching.service.CrewService; +import pairmatching.service.MatchingService; + +import javax.xml.parsers.ParserConfigurationException; +import java.io.IOException; + public class Application { - public static void main(String[] args) { - // TODO 구현 진행 + public static void main(String[] args) throws ParserConfigurationException, IOException, SAXException { + CrewService crewService = new CrewService(); + PairController pairController = new PairController( + crewService, + new MatchingService(crewService, new Matching()) + ); + + pairController.run(); } } diff --git a/src/main/java/pairmatching/controller/PairController.java b/src/main/java/pairmatching/controller/PairController.java new file mode 100644 index 000000000..670ced4f6 --- /dev/null +++ b/src/main/java/pairmatching/controller/PairController.java @@ -0,0 +1,95 @@ +package pairmatching.controller; + +import java.util.function.Supplier; + +import pairmatching.controller.dto.request.PairRequest; +import pairmatching.domain.Pairs; +import pairmatching.service.CrewService; +import pairmatching.service.MatchingService; +import pairmatching.view.ClearPairView; +import pairmatching.view.FunctionRequestView; +import pairmatching.view.PairRequestView; +import pairmatching.view.PairResponseView; +import pairmatching.view.RematchRequestView; +import pairmatching.view.console.ConsoleWriter; + +public class PairController { + private final CrewService crewService; + private final MatchingService matchingService; + + public PairController( + CrewService crewService, + MatchingService matchingService + ) { + this.crewService = crewService; + this.matchingService = matchingService; + } + + public void run() { + while (true) { + String input = FunctionRequestView.request(); + if (input == "Q") { + break; + } + if (input == "1") { + savePairs(); + } + if (input == "2") { + getPairs(); + } + if (input == "3") { + clearPairs(); + } + } + } + + // 페어 매칭 기능 + public void savePairs() { + PairRequest matchingRequest = getMatchingRequest(); + Pairs pairs = matchingService.generateMatching( + matchingRequest.course(), + matchingRequest.mission() + ); + PairResponseView.response(pairs); + } + + public PairRequest getMatchingRequest() { + while (true) { + PairRequest pairRequest = PairRequestView.request(); + if (matchingService.hasNoPairs(pairRequest.mission())) { + return pairRequest; + } + // 이미 매칭된 미션인 경우 + boolean rematch = RematchRequestView.request(); + if (rematch) { + return pairRequest; + } + } + } + + // 페어 조회 기능 + public void getPairs() { + Pairs pairs = retry(() -> { + return matchingService.getPairs( + PairRequestView.request().mission() + ); + }); + PairResponseView.response(pairs); + } + + // 페어 초기화 기능 + public void clearPairs() { + matchingService.clear(); + ClearPairView.response(); + } + + private static T retry(Supplier supplier) { + while (true) { + try { + return supplier.get(); + } catch (IllegalArgumentException e) { + ConsoleWriter.printlnMessage(e.getMessage()); + } + } + } +} diff --git a/src/main/java/pairmatching/controller/dto/request/PairRequest.java b/src/main/java/pairmatching/controller/dto/request/PairRequest.java new file mode 100644 index 000000000..09a052dd4 --- /dev/null +++ b/src/main/java/pairmatching/controller/dto/request/PairRequest.java @@ -0,0 +1,29 @@ +package pairmatching.controller.dto.request; + +import pairmatching.domain.constants.Course; +import pairmatching.domain.constants.Level; +import pairmatching.domain.constants.Mission; + +public class PairRequest { + private Course course; + private Level level; + private Mission mission; + + public PairRequest(Course course, Level level, Mission mission) { + this.course = course; + this.level = level; + this.mission = mission; + } + + public Course course() { + return course; + } + + public Level level() { + return level; + } + + public Mission mission() { + return mission; + } +} diff --git a/src/main/java/pairmatching/domain/Crews.java b/src/main/java/pairmatching/domain/Crews.java new file mode 100644 index 000000000..638efaaf6 --- /dev/null +++ b/src/main/java/pairmatching/domain/Crews.java @@ -0,0 +1,35 @@ +package pairmatching.domain; + +import pairmatching.domain.constants.Course; + +import java.util.ArrayList; +import java.util.List; + +public class Crews { + private List backend; + private List frontend; + + public Crews() { + this.backend = new ArrayList<>(); + this.frontend = new ArrayList<>(); + } + + public void add(Course course, List names) { + if (course.equals(Course.BACKEND)) { + backend.addAll(names); + } + if (course.equals(Course.FRONTEND)) { + frontend.addAll(names); + } + } + + public List getCrews(Course course) { + if (course.equals(Course.BACKEND)) { + return backend; + } + if (course.equals(Course.FRONTEND)) { + return frontend; + } + throw new IllegalStateException("적합하지 않은 과정 입력입니다."); + } +} diff --git a/src/main/java/pairmatching/domain/Matching.java b/src/main/java/pairmatching/domain/Matching.java new file mode 100644 index 000000000..39bbde949 --- /dev/null +++ b/src/main/java/pairmatching/domain/Matching.java @@ -0,0 +1,63 @@ +package pairmatching.domain; + +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +import pairmatching.domain.constants.Mission; +import pairmatching.global.exception.CustomException; +import pairmatching.global.exception.ErrorMessage; + +public class Matching { + private Map matching; + + public Matching() { + matching = new EnumMap<>(Mission.class); + } + + /** + * 페어 생성 + */ + public void save(Mission mission, Pairs pairs) { + matching.put(mission, pairs); + } + + /** + * 페어 조회 + */ + public Pairs getPairs(Mission mission) { + if (matching.containsKey(mission)) { + return matching.get(mission); + } + throw CustomException.from(ErrorMessage.NO_MATCHING_HISTORY); + } + + /** + * 페어 초기화 + */ + public void clear() { + matching.clear(); + } + + /** + * 매칭 정보가 있음을 확인하는 메서드 + */ + public boolean isEmpty(Mission enteredMission) { + return !matching.containsKey(enteredMission); + } + + public List getSameLevelPairs(Mission mission) { + List pairs = new ArrayList<>(); + for (Mission others : matching.keySet()) { + if (others.equals(mission)) { + continue; + } + + if (others.getLevel() == mission.getLevel()) { + pairs.add(matching.get(mission)); + } + } + return pairs; + } +} diff --git a/src/main/java/pairmatching/domain/Pair.java b/src/main/java/pairmatching/domain/Pair.java new file mode 100644 index 000000000..bb030cea3 --- /dev/null +++ b/src/main/java/pairmatching/domain/Pair.java @@ -0,0 +1,34 @@ +package pairmatching.domain; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +public class Pair { + private Set pair; + + public Pair() { + pair = new HashSet<>(); + } + + public void add(String name) { + pair.add(name); + } + + /** + * 두 페어가 중복 크루를 매칭하고 있음을 검증하는 메서드 + * @return 두 페어에 중복되는 크루가 두 명 이상 있으면 false, 그렇지 않다면 true + */ + public boolean duplicated(Pair other) { + Set combined = new HashSet<>(pair); + combined.retainAll(other.getPair()); + + return combined.size() >= 2; + } + + public Set getPair() { + return Collections.unmodifiableSet(pair); + } +} + + diff --git a/src/main/java/pairmatching/domain/Pairs.java b/src/main/java/pairmatching/domain/Pairs.java new file mode 100644 index 000000000..b6a4a869c --- /dev/null +++ b/src/main/java/pairmatching/domain/Pairs.java @@ -0,0 +1,42 @@ +package pairmatching.domain; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class Pairs { + private List pairs; + + public Pairs() { + this.pairs = new ArrayList<>(); + } + + public void add(Pair pair) { + pairs.add(pair); + } + + /** + * 서로 다른 미션의 매칭 정보를 비교하여, 중복되는 페어가 있는 지 확인 + */ + public boolean duplicated(Pairs others) { + for (Pair pair : pairs) { + if (duplicated(pair, others)) { + return false; + } + } + return true; + } + + private boolean duplicated(Pair pair, Pairs others) { + for (Pair other : others.getPairs()) { + if (pair.duplicated(other)) { + return false; + } + } + return true; + } + + public List getPairs() { + return Collections.unmodifiableList(pairs); + } +} diff --git a/src/main/java/pairmatching/domain/constants/Course.java b/src/main/java/pairmatching/domain/constants/Course.java new file mode 100644 index 000000000..365733c92 --- /dev/null +++ b/src/main/java/pairmatching/domain/constants/Course.java @@ -0,0 +1,28 @@ +package pairmatching.domain.constants; + +import pairmatching.global.exception.CustomException; +import pairmatching.global.exception.ErrorMessage; + +public enum Course { + BACKEND("백엔드"), + FRONTEND("프론트엔드"); + + private String name; + + Course(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public static Course getCourse(String name) { + for (Course course : Course.values()) { + if (course.name().equals(name)) { + return course; + } + } + throw CustomException.from(ErrorMessage.INVALID_PAIR_REQUEST); + } +} diff --git a/src/main/java/pairmatching/domain/constants/Level.java b/src/main/java/pairmatching/domain/constants/Level.java new file mode 100644 index 000000000..e4f4fe7dd --- /dev/null +++ b/src/main/java/pairmatching/domain/constants/Level.java @@ -0,0 +1,27 @@ +package pairmatching.domain.constants; + +import pairmatching.global.exception.CustomException; +import pairmatching.global.exception.ErrorMessage; + +public enum Level { + LEVEL1("레벨1"), + LEVEL2("레벨2"), + LEVEL3("레벨3"), + LEVEL4("레벨4"), + LEVEL5("레벨5"); + + private String name; + + Level(String name) { + this.name = name; + } + + public static Level getLevel(String name) { + for (Level level : Level.values()) { + if (level.name().equals(name)) { + return level; + } + } + throw CustomException.from(ErrorMessage.INVALID_PAIR_REQUEST); + } +} diff --git a/src/main/java/pairmatching/domain/constants/Mission.java b/src/main/java/pairmatching/domain/constants/Mission.java new file mode 100644 index 000000000..362607bd4 --- /dev/null +++ b/src/main/java/pairmatching/domain/constants/Mission.java @@ -0,0 +1,38 @@ +package pairmatching.domain.constants; + +import pairmatching.global.exception.CustomException; +import pairmatching.global.exception.ErrorMessage; + +public enum Mission { + RACING_CAR("자동차경주", Level.LEVEL1), + LOTTO("로또", Level.LEVEL1), + BASEBALL_GAME("숫자야구게임", Level.LEVEL1), + + SHOPPING_BASKET("장바구니", Level.LEVEL2), + PAYMENT("결제", Level.LEVEL2), + SUBWAY_MAP("지하철노선도", Level.LEVEL2), + + PERFORMANCE_IMPROVEMENT("성능개선", Level.LEVEL4), + DEPLOYMENT("배포", Level.LEVEL4); + + private final String name; + private final Level level; + + Mission(String name, Level level) { + this.name = name; + this.level = level; + } + + public Level getLevel() { + return level; + } + + public static Mission getMission(String name) { + for (Mission mission : Mission.values()) { + if (mission.name().equals(name)) { + return mission; + } + } + throw CustomException.from(ErrorMessage.INVALID_PAIR_REQUEST); + } +} diff --git a/src/main/java/pairmatching/global/exception/CustomException.java b/src/main/java/pairmatching/global/exception/CustomException.java new file mode 100644 index 000000000..32271d86d --- /dev/null +++ b/src/main/java/pairmatching/global/exception/CustomException.java @@ -0,0 +1,13 @@ +package pairmatching.global.exception; + +public class CustomException extends IllegalArgumentException { + private static final String PREFIX = "[ERROR] "; + + private CustomException(ErrorMessage errorMessage) { + super(PREFIX + errorMessage.getMessage()); + } + + public static CustomException from(ErrorMessage errorMessage) { + return new CustomException(errorMessage); + } +} diff --git a/src/main/java/pairmatching/global/exception/ErrorMessage.java b/src/main/java/pairmatching/global/exception/ErrorMessage.java new file mode 100644 index 000000000..50e0fb366 --- /dev/null +++ b/src/main/java/pairmatching/global/exception/ErrorMessage.java @@ -0,0 +1,19 @@ +package pairmatching.global.exception; + +public enum ErrorMessage { + NO_MATCHING_HISTORY("매칭 이력이 없습니다."), + BLANK_INPUT_ERROR("빈 문자열이 입력되었습니다."), + INVALID_PAIR_REQUEST("과정, 레벨, 미션을 다시 입력해주세요."), + INVALID_REMATCH_REQUEST("네 혹은 아니오를 입력해주세요."), + CANNOT_MATCHING_ERROR("페어 매칭에 실패하였습니다."), + INVALID_FUNCTION_REQUEST("잘못된 기능 번호를 입력하였습니다."); + private final String message; + + ErrorMessage(String message) { + this.message = message; + } + + public String getMessage() { + return this.message; + } +} diff --git a/src/main/java/pairmatching/service/CrewService.java b/src/main/java/pairmatching/service/CrewService.java new file mode 100644 index 000000000..17beaceba --- /dev/null +++ b/src/main/java/pairmatching/service/CrewService.java @@ -0,0 +1,34 @@ +package pairmatching.service; + +import org.xml.sax.SAXException; +import pairmatching.domain.Crews; +import pairmatching.domain.constants.Course; +import pairmatching.util.MarkdownParser; + +import javax.xml.parsers.ParserConfigurationException; +import java.io.IOException; +import java.util.List; + +public class CrewService { + private final Crews crews; + + public CrewService() throws ParserConfigurationException, IOException, SAXException { + this.crews = generateCrews(); + } + + public Crews generateCrews() throws ParserConfigurationException, IOException, SAXException { + MarkdownParser markdownParser = new MarkdownParser(); + List backend = markdownParser.parseName(Course.BACKEND); + List frontend = markdownParser.parseName(Course.FRONTEND); + + Crews crews = new Crews(); + crews.add(Course.BACKEND, backend); + crews.add(Course.FRONTEND, frontend); + + return crews; + } + + public List getCrews(Course course) { + return crews.getCrews(course); + } +} diff --git a/src/main/java/pairmatching/service/MatchingService.java b/src/main/java/pairmatching/service/MatchingService.java new file mode 100644 index 000000000..447944fdc --- /dev/null +++ b/src/main/java/pairmatching/service/MatchingService.java @@ -0,0 +1,102 @@ +package pairmatching.service; + +import java.util.List; + +import camp.nextstep.edu.missionutils.Randoms; +import pairmatching.domain.Matching; +import pairmatching.domain.Pair; +import pairmatching.domain.Pairs; +import pairmatching.domain.constants.Course; +import pairmatching.domain.constants.Mission; +import pairmatching.global.exception.CustomException; +import pairmatching.global.exception.ErrorMessage; + +public class MatchingService { + private static final int MAX_MATCHING_COUNT = 3; + private final CrewService crewService; + private Matching matching; + + public MatchingService(CrewService crewService, Matching matching) { + this.crewService = crewService; + this.matching = matching; + } + + public Pairs getPairs(Mission mission) { + return matching.getPairs(mission); + } + + /** + * 주어진 미션에 대한 매칭 정보가 없음을 검증하는 메서드 + * @return 매칭 정보가 없는 상태인 미션이라면 true, 그렇지 않다면 false + */ + public boolean hasNoPairs(Mission enteredMission) { + return matching.isEmpty(enteredMission); + } + + /** + * 3회에 걸쳐 페어를 생성하고, 유효한 매칭 정보인 경우 매칭 정보를 저장하는 메서드 + * @param mission 페어를 생성할 미션 + * @return 새로 생성한 페어. 매칭에 실패하면 에러를 반환 + */ + public Pairs generateMatching(Course course, Mission mission) { + for (int i = 0; i < MAX_MATCHING_COUNT; i++) { + Pairs generatedPairs = generatePairs(course); + boolean canMatch = hasNotDuplicatedMatching(mission, generatedPairs); + if (canMatch) { + matching.save(mission, generatedPairs); + return generatedPairs; + } + } + throw CustomException.from(ErrorMessage.CANNOT_MATCHING_ERROR); + } + + public Pairs generatePairs(Course course) { + Pairs pairs = new Pairs(); + List crews = Randoms.shuffle(crewService.getCrews(course)); + + // 크루의 수가 홀수인 경우 + if (crews.size() % 2 == 1 && crews.size() >= 3) { + pairs.add(generateTripleCrew(crews)); + } + for (int i = 0; i < crews.size(); i += 2) { + pairs.add(generateCoupleCrew(crews.get(i), crews.get(i + 1))); + } + + return pairs; + } + + private Pair generateCoupleCrew(String crew1, String crew2) { + Pair pair = new Pair(); + pair.add(crew1); + pair.add(crew2); + return pair; + } + + private Pair generateTripleCrew(List crews) { + Pair pair = new Pair(); + for (int i = 0; i < 3; i++) { + int last = crews.size() - 1; + pair.add(crews.get(last)); + crews.remove(last); + } + return pair; + } + + /** + * 현재 미션의 레벨의 미션들과 중복되는 매칭 정보가 있는 지 확인하는 메서드 + * @return 중복되는 매칭 정보가 없다면 true, 중복되는 매칭 정보가 있다면 false + */ + private boolean hasNotDuplicatedMatching(Mission mission, Pairs pairs) { + List sameLevelPairs = matching.getSameLevelPairs(mission); + for (Pairs others : sameLevelPairs) { + if (pairs.duplicated(others)) { + return false; + } + } + return true; + } + + public void clear() { + matching.clear(); + } +} diff --git a/src/main/java/pairmatching/util/MarkdownParser.java b/src/main/java/pairmatching/util/MarkdownParser.java new file mode 100644 index 000000000..8a2ebf67a --- /dev/null +++ b/src/main/java/pairmatching/util/MarkdownParser.java @@ -0,0 +1,28 @@ +package pairmatching.util; + +import org.xml.sax.SAXException; +import pairmatching.domain.constants.Course; + +import javax.xml.parsers.ParserConfigurationException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Locale; + +public class MarkdownParser { + public List parseName(Course course) throws ParserConfigurationException, IOException, SAXException { + String filePath = String.format( + "src/main/resources/%s-crew.md", + course.name().toLowerCase(Locale.ROOT) + ); + + return readMarkdownFile(filePath); + } + + private static List readMarkdownFile(String filePath) throws IOException { + Path path = Paths.get(filePath); + return Files.readAllLines(path); + } +} diff --git a/src/main/java/pairmatching/view/ClearPairView.java b/src/main/java/pairmatching/view/ClearPairView.java new file mode 100644 index 000000000..a11126ec7 --- /dev/null +++ b/src/main/java/pairmatching/view/ClearPairView.java @@ -0,0 +1,11 @@ +package pairmatching.view; + +import pairmatching.view.console.ConsoleWriter; + +public final class ClearPairView { + private static final String NOTICE = "초기화 되었습니다."; + + public static void response() { + ConsoleWriter.printlnMessage(NOTICE); + } +} diff --git a/src/main/java/pairmatching/view/FunctionRequestView.java b/src/main/java/pairmatching/view/FunctionRequestView.java new file mode 100644 index 000000000..fcb2fc1af --- /dev/null +++ b/src/main/java/pairmatching/view/FunctionRequestView.java @@ -0,0 +1,35 @@ +package pairmatching.view; + +import java.util.Arrays; +import java.util.List; + +import pairmatching.global.exception.CustomException; +import pairmatching.global.exception.ErrorMessage; +import pairmatching.view.console.ConsoleReader; +import pairmatching.view.console.ConsoleWriter; + +public final class FunctionRequestView { + private static final String NOTICE = "기능을 선택하세요."; + private static final String RESPONSE_TYPE = + "1. 페어 매칭\n" + + "2. 페어 조회\n" + + "3. 페어 초기화\n" + + "Q. 종료"; + private static final List RESPONSES = + Arrays.asList("1", "2", "3", "Q"); + + public static String request() { + ConsoleWriter.printlnMessage(NOTICE); + ConsoleWriter.printlnMessage(RESPONSE_TYPE); + return validate(ConsoleReader.enterMessage()); + } + + private static String validate(String enterMessage) { + for (String response : RESPONSES) { + if (response.equals(enterMessage)) { + return enterMessage; + } + } + throw CustomException.from(ErrorMessage.INVALID_FUNCTION_REQUEST); + } +} diff --git a/src/main/java/pairmatching/view/PairRequestView.java b/src/main/java/pairmatching/view/PairRequestView.java new file mode 100644 index 000000000..a7b81a1a2 --- /dev/null +++ b/src/main/java/pairmatching/view/PairRequestView.java @@ -0,0 +1,66 @@ +package pairmatching.view; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import pairmatching.controller.dto.request.PairRequest; +import pairmatching.domain.constants.Course; +import pairmatching.domain.constants.Level; +import pairmatching.domain.constants.Mission; +import pairmatching.global.exception.CustomException; +import pairmatching.global.exception.ErrorMessage; +import pairmatching.view.console.ConsoleReader; +import pairmatching.view.console.ConsoleWriter; + +/** + * 과정과 레벨, 미션을 출력하고 입력받는 뷰 + */ +public final class PairRequestView { + private static final String REQUEST_MESSAGE = "과정, 레벨, 미션을 선택하세요."; + private static final String REQUEST_EXAMPLE = "ex) 백엔드, 레벨1, 자동차경주"; + private static final String NOTICE = "" + + "#############################################\n" + + "과정: 백엔드 | 프론트엔드\n" + + "미션:\n" + + " - 레벨1: 자동차경주 | 로또 | 숫자야구게임\n" + + " - 레벨2: 장바구니 | 결제 | 지하철노선도\n" + + " - 레벨3: \n" + + " - 레벨4: 성능개선 | 배포\n" + + " - 레벨5: \n" + + "############################################"; + private static final String DIVISOR = ","; + + public static PairRequest request() { + ConsoleWriter.printlnMessage(NOTICE); + ConsoleWriter.printlnMessage(REQUEST_MESSAGE); + ConsoleWriter.printlnMessage(REQUEST_MESSAGE); + + return validate(ConsoleReader.enterMessage()); + } + + // 레벨과 미션의 입력이 맞지 않는 경우 예외처리 + private static PairRequest validate(String message) { + List input = split(message); + return new PairRequest( + Course.getCourse(input.get(0)), + Level.getLevel(input.get(1)), + Mission.getMission(input.get(2)) + ); + } + + private static List split(String message) { + String[] split = message.split(DIVISOR, -1); + if (split.length != 3) { + throw CustomException.from(ErrorMessage.INVALID_PAIR_REQUEST); + } + return new ArrayList<>( + Arrays.asList( + split[0].trim(), + split[1].trim(), + split[2].trim() + ) + ); + } + +} diff --git a/src/main/java/pairmatching/view/PairResponseView.java b/src/main/java/pairmatching/view/PairResponseView.java new file mode 100644 index 000000000..f0df2c78a --- /dev/null +++ b/src/main/java/pairmatching/view/PairResponseView.java @@ -0,0 +1,56 @@ +package pairmatching.view; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import pairmatching.domain.Pair; +import pairmatching.domain.Pairs; +import pairmatching.view.console.ConsoleWriter; + +public final class PairResponseView { + private static final String NOTICE = "페어 매칭 결과입니다."; + private static final String COUPLE_PAIR_RESPONSE = "%s : %s"; + private static final String TRIPLE_PAIR_RESPONSE = "%s : %s : %s"; + + public static void response(Pairs pairs) { + ConsoleWriter.printlnMessage(NOTICE); + printPairs(pairs); + } + + private static void printPairs(Pairs pairs) { + for (Pair pair : pairs.getPairs()) { + printPair(pair); + } + } + + private static void printPair(Pair pair) { + List names = getNames(pair.getPair()); + if (names.size() == 2) { // 수정 + ConsoleWriter.printlnFormat( + COUPLE_PAIR_RESPONSE, + names.get(0), + names.get(1) + ); + return; + } + if (names.size() == 3) { + ConsoleWriter.printlnFormat( + TRIPLE_PAIR_RESPONSE, + names.get(0), + names.get(1), + names.get(2) + ); + } + + throw new IllegalStateException("페어에 속한 크루의 수가 2 혹은 3이 아닙니다."); + } + + private static List getNames(Set crews) { + List names = new ArrayList<>(); + for (String crew : crews) { + names.add(crew); + } + return names; + } +} diff --git a/src/main/java/pairmatching/view/RematchRequestView.java b/src/main/java/pairmatching/view/RematchRequestView.java new file mode 100644 index 000000000..a56c08f0e --- /dev/null +++ b/src/main/java/pairmatching/view/RematchRequestView.java @@ -0,0 +1,29 @@ +package pairmatching.view; + +import pairmatching.global.exception.CustomException; +import pairmatching.global.exception.ErrorMessage; +import pairmatching.view.console.ConsoleReader; +import pairmatching.view.console.ConsoleWriter; + +public final class RematchRequestView { + private static final String NOTICE = "매칭 정보가 있습니다. 다시 매칭하시겠습니까?"; + private static final String EXAMPLE_RESPONSE = "네 | 아니오"; + private static final String YES = "네"; + private static final String NO = "아니오"; + + public static boolean request() { + ConsoleWriter.printlnMessage(NOTICE); + ConsoleWriter.printlnMessage(EXAMPLE_RESPONSE); + return validate(ConsoleReader.enterMessage()); + } + + private static boolean validate(String input) { + if (input == YES) { + return true; + } + if (input == NO) { + return false; + } + throw CustomException.from(ErrorMessage.INVALID_REMATCH_REQUEST); + } +} diff --git a/src/main/java/pairmatching/view/console/ConsoleReader.java b/src/main/java/pairmatching/view/console/ConsoleReader.java new file mode 100644 index 000000000..a887b72e4 --- /dev/null +++ b/src/main/java/pairmatching/view/console/ConsoleReader.java @@ -0,0 +1,24 @@ +package pairmatching.view.console; + +import camp.nextstep.edu.missionutils.Console; +import pairmatching.global.exception.CustomException; +import pairmatching.global.exception.ErrorMessage; + +public final class ConsoleReader { + public static String enterMessage() { + return Validator.validate(Console.readLine()); + } + + private static class Validator { + public static String validate(String message) { + validateBlankInput(message); + return message; + } + + private static void validateBlankInput(String message) { + if (message.length() == 0) { + throw CustomException.from(ErrorMessage.BLANK_INPUT_ERROR); + } + } + } +} diff --git a/src/main/java/pairmatching/view/console/ConsoleWriter.java b/src/main/java/pairmatching/view/console/ConsoleWriter.java new file mode 100644 index 000000000..d6ddf4341 --- /dev/null +++ b/src/main/java/pairmatching/view/console/ConsoleWriter.java @@ -0,0 +1,15 @@ +package pairmatching.view.console; + +public final class ConsoleWriter { + public static void printlnMessage(String message) { + System.out.println(message); + } + + public static void printlnFormat(String message, Object... args) { + printlnMessage(String.format(message, args)); + } + + public static void println() { + System.out.println(); + } +} diff --git a/src/test/java/pairmatching/ApplicationTest.java b/src/test/java/pairmatching/ApplicationTest.java index 2dff1e6d3..757cb008c 100644 --- a/src/test/java/pairmatching/ApplicationTest.java +++ b/src/test/java/pairmatching/ApplicationTest.java @@ -1,41 +1,53 @@ package pairmatching; -import static camp.nextstep.edu.missionutils.test.Assertions.assertShuffleTest; -import static camp.nextstep.edu.missionutils.test.Assertions.assertSimpleTest; -import static org.assertj.core.api.Assertions.assertThat; +import static camp.nextstep.edu.missionutils.test.Assertions.*; +import static org.assertj.core.api.Assertions.*; -import camp.nextstep.edu.missionutils.test.NsTest; +import java.io.IOException; import java.util.Arrays; + +import javax.xml.parsers.ParserConfigurationException; + import org.junit.jupiter.api.Test; -import pairmatching.Application; +import org.xml.sax.SAXException; + +import camp.nextstep.edu.missionutils.test.NsTest; class ApplicationTest extends NsTest { - private static final String ERROR_MESSAGE = "[ERROR]"; - - @Test - void 짝수_인원_페어_매칭() { - assertShuffleTest( - () -> { - run("1", "백엔드, 레벨1, 자동차경주", "Q"); - assertThat(output()).contains("태웅 : 백호", "치수 : 태섭"); - }, - Arrays.asList("태웅", "백호", "치수", "태섭") - ); - } - - @Test - void 없는_미션에_대한_예외_처리() { - assertSimpleTest( - () -> { - runException("1", "백엔드, 레벨1, 오징어게임"); - assertThat(output()).contains(ERROR_MESSAGE); - } - ); - } - - @Override - public void runMain() { - Application.main(new String[]{}); - } + private static final String ERROR_MESSAGE = "[ERROR]"; + + @Test + void 짝수_인원_페어_매칭() { + assertShuffleTest( + () -> { + run("1", "백엔드, 레벨1, 자동차경주", "Q"); + assertThat(output()).contains("태웅 : 백호", "치수 : 태섭"); + }, + Arrays.asList("태웅", "백호", "치수", "태섭") + ); + } + + @Test + void 없는_미션에_대한_예외_처리() { + assertSimpleTest( + () -> { + runException("1", "백엔드, 레벨1, 오징어게임"); + assertThat(output()).contains(ERROR_MESSAGE); + } + ); + } + + @Override + public void runMain() { + try { + Application.main(new String[] {}); + } catch (ParserConfigurationException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new RuntimeException(e); + } catch (SAXException e) { + throw new RuntimeException(e); + } + } }